diff --git a/.github/workflows/linux_server_build.yml b/.github/workflows/linux_server_build.yml
index 51bf4ae3bc..8cf2d46169 100644
--- a/.github/workflows/linux_server_build.yml
+++ b/.github/workflows/linux_server_build.yml
@@ -1,6 +1,6 @@
 # Copyright 2013-2019 High Fidelity, Inc.
 # Copyright 2020-2022 Vircadia contributors.
-# Copyright 2021-2023 Overte e.V.
+# Copyright 2021-2024 Overte e.V.
 # SPDX-License-Identifier: Apache-2.0
 
 name: Linux Server CI Build
@@ -12,6 +12,10 @@ on:
   push:
     branches:
       - master
+    tags:
+      # Release tags. E.g. 2024.06.1
+      # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet
+      - "[0-9][0-9][0-9][0-9].[0-9][0-9].**"
 
 env:
   BUILD_TYPE: Release
@@ -19,8 +23,8 @@ env:
   UPLOAD_BUCKET: overte-public
   UPLOAD_REGION: fra1
   UPLOAD_ENDPOINT: "https://fra1.digitaloceanspaces.com"
-  CMAKE_BACKTRACE_URL: ${{ secrets.SENTRY_MINIDUMP_ENDPOINT }}
-  CMAKE_BACKTRACE_TOKEN: server_${{ github.event.number }}_${{ github.sha }}
+  # Disable VCPKG caching to save time.
+  VCPKG_FEATURE_FLAGS: -binarycaching
 
 jobs:
   build:
@@ -33,62 +37,77 @@ jobs:
           - os: debian-11
             image: docker.io/overte/overte-server-build:0.1.3-debian-11-amd64
             arch: amd64
-            runner: linux_amd64
+            # https://github.com/testflows/TestFlows-GitHub-Hetzner-Runners/wiki/Meta-Labels
+            # self_hosted makes the Hetzner auto-scaler put up the job.
+            # type-cx52 is a Hetzner VPS server type. In this case cs52 is a server with 16-cores and 32GB of RAM.
+            # image-x86-app-docker-ce is a Hetzner image.
+            # https://github.com/testflows/TestFlows-GitHub-Hetzner-Runners/wiki/Specifying-The-Runner-Image
+            runner: [self_hosted, type-cx52, image-x86-app-docker-ce]
 
           - os: debian-11
             image: docker.io/overte/overte-server-build:0.1.3-debian-11-aarch64
             arch: aarch64
-            runner: linux_aarch64
+            runner: [self_hosted, type-cax41, image-arm-app-docker-ce]
 
           - os: debian-12
             image: docker.io/overte/overte-server-build:0.1.3-debian-12-amd64
             arch: amd64
-            runner: linux_amd64
+            runner: [self_hosted, type-cx52, image-x86-app-docker-ce]
 
           - os: debian-12
             image: docker.io/overte/overte-server-build:0.1.3-debian-12-aarch64
             arch: aarch64
-            runner: linux_aarch64
+            runner: [self_hosted, type-cax41, image-arm-app-docker-ce]
 
           - os: ubuntu-20.04
             image: docker.io/overte/overte-server-build:0.1.3-ubuntu-20.04-amd64
             arch: amd64
-            runner: linux_amd64
+            runner: [self_hosted, type-cx52, image-x86-app-docker-ce]
 
           - os: ubuntu-22.04
             image: docker.io/overte/overte-server-build:0.1.3-ubuntu-22.04-amd64
             arch: amd64
-            runner: linux_amd64
+            runner: [self_hosted, type-cx52, image-x86-app-docker-ce]
 
           - os: ubuntu-22.04
             image: docker.io/overte/overte-server-build:0.1.3-ubuntu-22.04-aarch64
             arch: aarch64
-            runner: linux_aarch64
+            runner: [self_hosted, type-cax41, image-arm-app-docker-ce]
 
-          - os: fedora-37
-            image: docker.io/overte/overte-server-build:0.1.3-fedora-37-amd64
+          - os: ubuntu-24.04
+            image: docker.io/overte/overte-server-build:0.1.3-ubuntu-24.04-amd64
             arch: amd64
-            runner: linux_amd64
+            runner: [self_hosted, type-cx52, image-x86-app-docker-ce]
 
-          - os: fedora-37
-            image: docker.io/overte/overte-server-build:0.1.3-fedora-37-aarch64
+          - os: ubuntu-24.04
+            image: docker.io/overte/overte-server-build:0.1.3-ubuntu-24.04-aarch64
             arch: aarch64
-            runner: linux_aarch64
+            runner: [self_hosted, type-cax41, image-arm-app-docker-ce]
 
-          - os: fedora-38
-            image: docker.io/overte/overte-server-build:0.1.3-fedora-38-amd64
+          - os: fedora-39
+            image: docker.io/overte/overte-server-build:0.1.4-fedora-39-amd64
             arch: amd64
-            runner: linux_amd64
+            runner: [self_hosted, type-cx52, image-x86-app-docker-ce]
 
-          - os: fedora-38
-            image: docker.io/overte/overte-server-build:0.1.3-fedora-38-aarch64
+          - os: fedora-39
+            image: docker.io/overte/overte-server-build:0.1.4-fedora-39-aarch64
             arch: aarch64
-            runner: linux_aarch64
+            runner: [self_hosted, type-cax41, image-arm-app-docker-ce]
+
+          - os: fedora-40
+            image: docker.io/overte/overte-server-build:0.1.4-fedora-39-amd64
+            arch: amd64
+            runner: [self_hosted, type-cx52, image-x86-app-docker-ce]
+
+          - os: fedora-40
+            image: docker.io/overte/overte-server-build:0.1.4-fedora-39-aarch64
+            arch: aarch64
+            runner: [self_hosted, type-cax41, image-arm-app-docker-ce]
 
           - os: rockylinux-9
             image: docker.io/overte/overte-server-build:0.1.3-rockylinux-9-amd64
             arch: amd64
-            runner: linux_amd64
+            runner: [self_hosted, type-cx52, image-x86-app-docker-ce]
 
       fail-fast: false
 
@@ -132,7 +151,7 @@ jobs:
         fi
 
         # Tagged builds. E.g. release or release candidate builds.
-        if [ "${{github.event_name}}" != "pull_request" ]; then
+        if [ "${{github.ref_type}}" == "tag" ]; then
           echo "PRODUCTION_BUILD=true" >> $GITHUB_ENV
         fi
 
@@ -166,16 +185,30 @@ jobs:
           echo "UPLOAD_PREFIX=build/overte/master" >> $GITHUB_ENV
           echo "RELEASE_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV
         else # tagged
-          echo "DEBVERSION=${{ github.run_number }}-${{ github.ref_name }}-$GIT_COMMIT_SHORT-${{ matrix.os }}" >> $GITHUB_ENV
-          echo "RPMVERSION=${${{ github.ref_name }}//-/.}.${{ github.run_number }}.$GIT_COMMIT_SHORT" >> $GITHUB_ENV
+          echo "DEBVERSION=${{ github.ref_name }}-$GIT_COMMIT_SHORT-${{ matrix.os }}" >> $GITHUB_ENV
+          echo "RPMVERSION=${{ github.ref_name }}.$GIT_COMMIT_SHORT" >> $GITHUB_ENV
         fi
 
-        if [[ "${{ github.ref_name }}" != "master" && "${{ github.ref_name }}" != "pull_request" ]]; then  # tagged
-          echo "RELEASE_NUMBER=/${{ github.ref_name }}" >> $GITHUB_ENV
-          if [ "${{ github.ref_name }}" == *"rc"* ]; then  # release candidate
-            echo "UPLOAD_PREFIX=build/overte/release-candidate" >> $GITHUB_ENV
+        if [ "${{ github.ref_type }}" == "tag" ]; then  # tagged
+          echo "RELEASE_NUMBER=${{ github.ref_name }}" >> $GITHUB_ENV
+          if [[ "${{ github.ref_name }}" == *"rc"* ]]; then  # release candidate
+            # The uploader already creates a subfolder for each RELEASE_NUMBER.
+            echo "UPLOAD_PREFIX=build/overte/release-candidate/" >> $GITHUB_ENV
           else  # release
-            echo "UPLOAD_PREFIX=build/overte/release" >> $GITHUB_ENV
+            echo "UPLOAD_PREFIX=build/overte/release/" >> $GITHUB_ENV
+          fi
+        fi
+
+        echo "BUILD_NUMBER=$GIT_COMMIT_SHORT" >> $GITHUB_ENV
+
+        if [ -z "$CMAKE_BACKTRACE_URL" ]; then
+          if [ "${{ github.ref_type }}" == "tag" ]; then
+            export CMAKE_BACKTRACE_URL="${{ secrets.SENTRY_MINIDUMP_ENDPOINT }}"
+            export CMAKE_BACKTRACE_TOKEN="${{ github.ref_name }}_${{ matrix.os }}_${{ github.sha }}"
+          else
+            # We're building a PR, default to the PR endpoint
+            export CMAKE_BACKTRACE_URL="https://o4504831972343808.ingest.sentry.io/api/4504832427950080/minidump/?sentry_key=f511de295975461b8f92a36f4a4a4f32"
+            export CMAKE_BACKTRACE_TOKEN="server_pr_${{ github.event.number }}_${{ github.sha }}"
           fi
         fi
 
@@ -192,10 +225,12 @@ jobs:
         else  # RPM
           if [ "${{ matrix.os }}" == "rockylinux-9" ]; then
             echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.el9.$INSTALLER_EXT" >> $GITHUB_ENV
-          elif [ "${{ matrix.os }}" == "fedora-37" ]; then
-            echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.fc37.$INSTALLER_EXT" >> $GITHUB_ENV
           elif [ "${{ matrix.os }}" == "fedora-38" ]; then
             echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.fc38.$INSTALLER_EXT" >> $GITHUB_ENV
+          elif [ "${{ matrix.os }}" == "fedora-39" ]; then
+            echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.fc39.$INSTALLER_EXT" >> $GITHUB_ENV
+          elif [ "${{ matrix.os }}" == "fedora-40" ]; then
+            echo "ARTIFACT_PATTERN=overte-server-$RPMVERSION-1.fc40.$INSTALLER_EXT" >> $GITHUB_ENV
           else
             echo "Error! ARTIFACT_PATTERN not set!"
             exit 1  # Fail
@@ -203,7 +238,7 @@ jobs:
         fi
 
 
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: false
         fetch-depth: 1
@@ -216,11 +251,6 @@ jobs:
       working-directory: build
       shell: bash
       run: |
-        if [ -z "$CMAKE_BACKTRACE_URL" ] ; then
-          # We're building a PR, default to the PR endpoint
-          export CMAKE_BACKTRACE_URL="https://o4504831972343808.ingest.sentry.io/api/4504832427950080/minidump/?sentry_key=f511de295975461b8f92a36f4a4a4f32"
-          export CMAKE_BACKTRACE_TOKEN="server_pr_${{ github.event.number }}_${{ github.sha }}"
-        fi
 
         cmake .. -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DVCPKG_BUILD_TYPE=release $CMAKE_EXTRA
 
@@ -275,8 +305,7 @@ jobs:
         df -h
 
     - name: Upload artifact to GitHub
-      if: github.event_name == 'pull_request'
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: ${{ env.ARTIFACT_PATTERN }}
         path: pkg-scripts/${{ env.ARTIFACT_PATTERN }}
diff --git a/.github/workflows/master_build.yml b/.github/workflows/master_build.yml
index 0f8e719094..265adde87c 100644
--- a/.github/workflows/master_build.yml
+++ b/.github/workflows/master_build.yml
@@ -1,6 +1,6 @@
 # Copyright 2013-2019 High Fidelity, Inc.
 # Copyright 2020-2022 Vircadia contributors
-# Copyright 2021-2022 Overte e.V.
+# Copyright 2021-2024 Overte e.V.
 # SPDX-License-Identifier: Apache-2.0
 
 name: Master CI Build
@@ -26,6 +26,8 @@ env:
   UPLOAD_ENDPOINT: "https://fra1.digitaloceanspaces.com"
   CMAKE_BACKTRACE_URL: ${{ secrets.SENTRY_MINIDUMP_ENDPOINT }}
   CMAKE_BACKTRACE_TOKEN: master_${{ github.event.number }}_${{ github.sha }}
+  # Disable VCPKG caching to save time.
+  VCPKG_FEATURE_FLAGS: -binarycaching
 
   # OSX-specific variables
   DEVELOPER_DIR: /Applications/Xcode_11.2.app/Contents/Developer
@@ -120,7 +122,7 @@ jobs:
         rm -rf ~/overte-files
         rm -rf ~/.cache
 
-    - uses: actions/checkout@v1
+    - uses: actions/checkout@v4
       with:
         submodules: false
         fetch-depth: 1
diff --git a/.github/workflows/master_deploy_apidocs.yml b/.github/workflows/master_deploy_apidocs.yml
index e8d21f9d97..79d4533b40 100644
--- a/.github/workflows/master_deploy_apidocs.yml
+++ b/.github/workflows/master_deploy_apidocs.yml
@@ -1,4 +1,4 @@
-# Copyright 2022-2023 Overte e.V.
+# Copyright 2022-2024 Overte e.V.
 # SPDX-License-Identifier: Apache-2.0
 
 name: Master API-docs CI Build and Deploy
@@ -14,7 +14,7 @@ jobs:
 
     name: Build and deploy API-docs
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
     - name: Install dependencies
       working-directory: tools/jsdoc
diff --git a/.github/workflows/master_deploy_doxygen.yml b/.github/workflows/master_deploy_doxygen.yml
index ea99e265bb..9a12a165a8 100644
--- a/.github/workflows/master_deploy_doxygen.yml
+++ b/.github/workflows/master_deploy_doxygen.yml
@@ -1,4 +1,4 @@
-# Copyright 2022-2023 Overte e.V.
+# Copyright 2022-2024 Overte e.V.
 # SPDX-License-Identifier: Apache-2.0
 
 name: Master Doxygen CI Build and Deploy
@@ -14,7 +14,7 @@ jobs:
 
     name: Build and deploy Doxygen documentation
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
 
     - name: Install dependencies
       run: |
diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml
index d9548fd862..2bd3b629c4 100644
--- a/.github/workflows/pr_build.yml
+++ b/.github/workflows/pr_build.yml
@@ -1,6 +1,6 @@
 # Copyright 2013-2019 High Fidelity, Inc.
 # Copyright 2020-2022 Vircadia contributors.
-# Copyright 2021-2022 Overte e.V.
+# Copyright 2021-2024 Overte e.V.
 # SPDX-License-Identifier: Apache-2.0
 
 name: Pull Request CI Build
@@ -24,6 +24,8 @@ env:
   # We can't use secrets or actions here, so the actual value has to be hardcoded.
   CMAKE_BACKTRACE_URL: "https://o4504831972343808.ingest.sentry.io/api/4504832427950080/minidump/?sentry_key=f511de295975461b8f92a36f4a4a4f32"
   CMAKE_BACKTRACE_TOKEN: PR_${{ github.event.number }}_${{ github.sha }}
+  # Disable VCPKG caching to save time.
+  VCPKG_FEATURE_FLAGS: -binarycaching
 
   UPLOAD_BUCKET: overte-public
   UPLOAD_REGION: fra1
@@ -54,7 +56,12 @@ jobs:
             #- os: macOS-10.15
             #  build_type: full
             - os: Ubuntu 20.04
-              runner: linux_amd64
+              # https://github.com/testflows/TestFlows-GitHub-Hetzner-Runners/wiki/Meta-Labels
+              # self_hosted makes the Hetzner auto-scaler put up the job.
+              # type-cx52 is a Hetzner VPS server type. In this case cs52 is a server with 16-cores and 32GB of RAM.
+              # image-x86-app-docker-ce is a Hetzner image.
+              # https://github.com/testflows/TestFlows-GitHub-Hetzner-Runners/wiki/Specifying-The-Runner-Image
+              runner: [self_hosted, type-cx52, image-x86-app-docker-ce]
               arch: amd64
               build_type: full
               apt-dependencies: pkg-config libxext-dev libdouble-conversion-dev libpcre2-16-0 libpulse0 libharfbuzz-dev libnss3 libnspr4 libxdamage1 libasound2 # add missing dependencies to docker image when convenient
@@ -65,7 +72,7 @@ jobs:
             #  apt-dependencies: mesa-common-dev libegl1 libglvnd-dev libdouble-conversion1 libpulse0 python3-github python3-distro
             # Do not change the names of self-hosted runners without knowing what you are doing, as they correspond to labels that have to be set on the runner.
             - os: Ubuntu 22.04
-              runner: linux_aarch64
+              runner: [self_hosted, type-cax41, image-arm-app-docker-ce]
               arch: aarch64
               build_type: full
               image: docker.io/overte/overte-full-build:0.1.1-ubuntu-22.04-aarch64
@@ -165,7 +172,7 @@ jobs:
         rm -rf ~/overte-files
         rm -rf ~/.cache
 
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v4
       with:
         submodules: false
         fetch-depth: 1
@@ -335,7 +342,7 @@ jobs:
 
     - name: Upload Artifact
       if: startsWith(matrix.os, 'Windows') || startsWith(matrix.os, 'macOS')
-      uses: actions/upload-artifact@v3
+      uses: actions/upload-artifact@v4
       with:
         name: ${{ env.ARTIFACT_PATTERN }}
         path: ./build/${{ env.ARTIFACT_PATTERN }}
diff --git a/BUILD_ANDROID.md b/BUILD_ANDROID.md
index 8a0b9da260..7bceb54cad 100644
--- a/BUILD_ANDROID.md
+++ b/BUILD_ANDROID.md
@@ -8,6 +8,8 @@ SPDX-License-Identifier: Apache-2.0
 # Build Android
 
 *Last Updated on December 15, 2020*
+> [!WARNING]  
+> Android building is currently broken, due to breaking changes in Qt and Gradle. Help with updating (or rewriting) the Gradle scripts would be great.
 
 Please read the [general build guide](BUILD.md) for information on building other platforms. Only Android specific instructions are found in this file. **Note that these instructions apply to building for the Oculus Quest 1.**
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 33e30fa6e4..a46f1963ef 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -15,6 +15,9 @@ endif()
 # 3.14 is the minimum version that supports symlinks on Windows
 cmake_minimum_required(VERSION 3.14)
 
+# This should allow using long paths on Windows
+SET(CMAKE_NINJA_FORCE_RESPONSE_FILE 1 CACHE INTERNAL "")
+
 # Passing of variables to vcpkg
 #
 # vcpkg runs cmake scripts in an isolated environment, see this for details:
@@ -181,8 +184,13 @@ if(OVERTE_WARNINGS_WHITELIST)
 endif()
 
 if(OVERTE_WARNINGS_AS_ERRORS)
-    set(ENV{CXXFLAGS} "$ENV{CXXFLAGS} -Werror")
-    set(ENV{CFLAGS} "$ENV{CXXFLAGS} -Werror")
+    if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC" OR (CMAKE_CXX_COMPILER_ID MATCHES "" AND WIN32))
+      set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /WX")
+      set(CMAKE_CFLAGS "${CMAKE_CFLAGS} /WX")
+    else()
+      set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror")
+      set(CMAKE_CFLAGS "${CMAKE_CFLAGS} -Werror")
+    endif()
 endif()
 
 
@@ -249,9 +257,6 @@ else()
 endif()
 
 set(SCREENSHARE 0)
-if (WIN32)
-  set(SCREENSHARE 1)
-endif()
 if (APPLE AND NOT CLIENT_ONLY)
   # Don't include Screenshare in OSX client-only builds.
   set(SCREENSHARE 1)
@@ -483,6 +488,7 @@ if (BUILD_CLIENT)
   endif()
 
   option(USE_SIXENSE "Build Interface with sixense library/plugin" OFF)
+  option(USE_NEURON "Build Interface with Neuron library/plugin" OFF)
 endif()
 
 if (BUILD_CLIENT OR BUILD_SERVER)
diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java
index ef9876c71a..0703fabf02 100644
--- a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java
+++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java
@@ -109,7 +109,7 @@ public class PermissionChecker extends Activity {
                     JSONObject obj = new JSONObject();
                         try {
                             obj.put("firstRun",false);
-                            obj.put("Avatar/fullAvatarURL", avatarPaths[which]);
+                            obj.put(SETTINGS_FULL_PRIVATE_GROUP_NAME + "/Avatar/fullAvatarURL", avatarPaths[which]);
                             File directory = new File(pathForJson);
 
                             if(!directory.exists()) directory.mkdirs();
diff --git a/assignment-client/src/AgentScriptingInterface.h b/assignment-client/src/AgentScriptingInterface.h
index cd5aa5ad65..1146c4006b 100644
--- a/assignment-client/src/AgentScriptingInterface.h
+++ b/assignment-client/src/AgentScriptingInterface.h
@@ -40,7 +40,7 @@
  */
 class AgentScriptingInterface : public QObject {
     Q_OBJECT
-    Q_PROPERTY(bool isAvatar READ isAvatar WRITE setIsAvatar)
+    Q_PROPERTY(bool isAvatar READ getIsAvatar WRITE setIsAvatar)
     Q_PROPERTY(bool isPlayingAvatarSound READ isPlayingAvatarSound)
     Q_PROPERTY(bool isListeningToAudioStream READ isListeningToAudioStream WRITE setIsListeningToAudioStream)
     Q_PROPERTY(bool isNoiseGateEnabled READ isNoiseGateEnabled WRITE setIsNoiseGateEnabled)
@@ -77,15 +77,15 @@ public slots:
 
     /*@jsdoc
      * Checks whether the script is emulating an avatar.
-     * @function Agent.isAvatar
+     * @function Agent.getIsAvatar
      * @returns {boolean} <code>true</code> if the script is emulating an avatar, otherwise <code>false</code>.
      * @example <caption>Check whether the agent is emulating an avatar.</caption>
      * (function () {
-     *     print("Agent is avatar: " + Agent.isAvatar());
+     *     print("Agent is avatar: " + Agent.getIsAvatar());
      *     print("Agent is avatar: " + Agent.isAvatar); // Same result.
      * }());
      */
-    bool isAvatar() const { return _agent->isAvatar(); }
+    bool getIsAvatar() const { return _agent->isAvatar(); }
 
     /*@jsdoc
      * Plays a sound from the position and with the orientation of the emulated avatar's head. No sound is played unless 
diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp
index 470ab3f233..a47420a873 100644
--- a/assignment-client/src/audio/AudioMixerClientData.cpp
+++ b/assignment-client/src/audio/AudioMixerClientData.cpp
@@ -222,13 +222,23 @@ void AudioMixerClientData::parseInjectorGainSet(ReceivedMessage& message, const
     qCDebug(audio) << "Setting MASTER injector gain for" << uuid << "to" << gain;
 }
 
-void AudioMixerClientData::setGainForAvatar(QUuid nodeID, float gain) {
-    auto it = std::find_if(_streams.active.cbegin(), _streams.active.cend(), [nodeID](const MixableStream& mixableStream){
+bool setGainInStreams(const QUuid &nodeID, float gain, std::vector<AudioMixerClientData::MixableStream> &streamVector) {
+    auto itActive = std::find_if(streamVector.cbegin(), streamVector.cend(),
+                                 [nodeID](const AudioMixerClientData::MixableStream& mixableStream){
         return mixableStream.nodeStreamID.nodeID == nodeID && mixableStream.nodeStreamID.streamID.isNull();
     });
 
-    if (it != _streams.active.cend()) {
-        it->hrtf->setGainAdjustment(gain);
+    if (itActive != streamVector.cend()) {
+        itActive->hrtf->setGainAdjustment(gain);
+        return true;
+    } else {
+        return false;
+    }
+}
+
+void AudioMixerClientData::setGainForAvatar(QUuid nodeID, float gain) {
+    if (!setGainInStreams(nodeID, gain, _streams.active)) {
+        setGainInStreams(nodeID, gain, _streams.inactive);
     }
 }
 
diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp
index c919d2a2d7..a1db33fbc8 100644
--- a/assignment-client/src/avatars/ScriptableAvatar.cpp
+++ b/assignment-client/src/avatars/ScriptableAvatar.cpp
@@ -30,8 +30,12 @@
 #include <NetworkingConstants.h>
 
 
-ScriptableAvatar::ScriptableAvatar(): _scriptEngine(newScriptEngine()) {
+ScriptableAvatar::ScriptableAvatar() {
     _clientTraitsHandler.reset(new ClientTraitsHandler(this));
+    static std::once_flag once;
+    std::call_once(once, [] {
+        qRegisterMetaType<HFMModel::Pointer>("HFMModel::Pointer");
+    });
 }
 
 QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) {
@@ -52,6 +56,7 @@ void ScriptableAvatar::startAnimation(const QString& url, float fps, float prior
     _animation = DependencyManager::get<AnimationCache>()->getAnimation(url);
     _animationDetails = AnimationDetails("", QUrl(url), fps, 0, loop, hold, false, firstFrame, lastFrame, true, firstFrame, false);
     _maskedJoints = maskedJoints;
+    _isAnimationRigValid = false;
 }
 
 void ScriptableAvatar::stopAnimation() {
@@ -89,11 +94,12 @@ QStringList ScriptableAvatar::getJointNames() const {
 }
 
 void ScriptableAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
-    _bind.reset();
-    _animSkeleton.reset();
+    _avatarAnimSkeleton.reset();
+    _geometryResource.reset();
 
     AvatarData::setSkeletonModelURL(skeletonModelURL);
     updateJointMappings();
+    _isRigValid = false;
 }
 
 int ScriptableAvatar::sendAvatarDataPacket(bool sendAll) {
@@ -137,65 +143,87 @@ static AnimPose composeAnimPose(const HFMJoint& joint, const glm::quat rotation,
 }
 
 void ScriptableAvatar::update(float deltatime) {
+    if (!_geometryResource && !_skeletonModelFilenameURL.isEmpty()) { // AvatarData will parse the .fst, but not get the .fbx skeleton.
+        _geometryResource = DependencyManager::get<ModelCache>()->getGeometryResource(_skeletonModelFilenameURL);
+    }
+
     // Run animation
-    if (_animation && _animation->isLoaded() && _animation->getFrames().size() > 0 && !_bind.isNull() && _bind->isLoaded()) {
-        if (!_animSkeleton) {
-            _animSkeleton = std::make_shared<AnimSkeleton>(_bind->getHFMModel());
-        }
-        float currentFrame = _animationDetails.currentFrame + deltatime * _animationDetails.fps;
-        if (_animationDetails.loop || currentFrame < _animationDetails.lastFrame) {
-            while (currentFrame >= _animationDetails.lastFrame) {
-                currentFrame -= (_animationDetails.lastFrame - _animationDetails.firstFrame);
+    Q_ASSERT(QThread::currentThread() == thread());
+    if (_animation && _animation->isLoaded()) {
+        Q_ASSERT(thread() == _animation->thread());
+        auto frames = _animation->getFramesReference();
+        if (frames.size() > 0 && _geometryResource && _geometryResource->isHFMModelLoaded()) {
+            if (!_isRigValid) {
+                _rig.reset(_geometryResource->getHFMModel());
+                _isRigValid = true;
             }
-            _animationDetails.currentFrame = currentFrame;
-
-            const QVector<HFMJoint>& modelJoints = _bind->getHFMModel().joints;
-            QStringList animationJointNames = _animation->getJointNames();
-
-            const int nJoints = modelJoints.size();
-            if (_jointData.size() != nJoints) {
-                _jointData.resize(nJoints);
+            if (!_isAnimationRigValid) {
+                _animationRig.reset(_animation->getHFMModel());
+                _isAnimationRigValid = true;
             }
-
-            const int frameCount = _animation->getFrames().size();
-            const HFMAnimationFrame& floorFrame = _animation->getFrames().at((int)glm::floor(currentFrame) % frameCount);
-            const HFMAnimationFrame& ceilFrame = _animation->getFrames().at((int)glm::ceil(currentFrame) % frameCount);
-            const float frameFraction = glm::fract(currentFrame);
-            std::vector<AnimPose> poses = _animSkeleton->getRelativeDefaultPoses();
-
-            const float UNIT_SCALE = 0.01f;
-
-            for (int i = 0; i < animationJointNames.size(); i++) {
-                const QString& name = animationJointNames[i];
-                // As long as we need the model preRotations anyway, let's get the jointIndex from the bind skeleton rather than
-                // trusting the .fst (which is sometimes not updated to match changes to .fbx).
-                int mapping = _bind->getHFMModel().getJointIndex(name);
-                if (mapping != -1 && !_maskedJoints.contains(name)) {
-
-                    AnimPose floorPose = composeAnimPose(modelJoints[mapping], floorFrame.rotations[i], floorFrame.translations[i] * UNIT_SCALE);
-                    AnimPose ceilPose = composeAnimPose(modelJoints[mapping], ceilFrame.rotations[i], floorFrame.translations[i] * UNIT_SCALE);
-                    blend(1, &floorPose, &ceilPose, frameFraction, &poses[mapping]);
-                 }
+            if (!_avatarAnimSkeleton) {
+                _avatarAnimSkeleton = std::make_shared<AnimSkeleton>(_geometryResource->getHFMModel());
             }
-
-            std::vector<AnimPose> absPoses = poses;
-            _animSkeleton->convertRelativePosesToAbsolute(absPoses);
-            for (int i = 0; i < nJoints; i++) {
-                JointData& data = _jointData[i];
-                AnimPose& absPose = absPoses[i];
-                if (data.rotation != absPose.rot()) {
-                    data.rotation = absPose.rot();
-                    data.rotationIsDefaultPose = false;
+            float currentFrame = _animationDetails.currentFrame + deltatime * _animationDetails.fps;
+            if (_animationDetails.loop || currentFrame < _animationDetails.lastFrame) {
+                while (currentFrame >= _animationDetails.lastFrame) {
+                    currentFrame -= (_animationDetails.lastFrame - _animationDetails.firstFrame);
                 }
-                AnimPose& relPose = poses[i];
-                if (data.translation != relPose.trans()) {
-                    data.translation = relPose.trans();
-                    data.translationIsDefaultPose = false;
-                }
-            }
+                _animationDetails.currentFrame = currentFrame;
 
-        } else {
-            _animation.clear();
+                const QVector<HFMJoint>& modelJoints = _geometryResource->getHFMModel().joints;
+                QStringList animationJointNames = _animation->getJointNames();
+
+                const int nJoints = modelJoints.size();
+                if (_jointData.size() != nJoints) {
+                    _jointData.resize(nJoints);
+                }
+
+                const int frameCount = frames.size();
+                const HFMAnimationFrame& floorFrame = frames.at((int)glm::floor(currentFrame) % frameCount);
+                const HFMAnimationFrame& ceilFrame = frames.at((int)glm::ceil(currentFrame) % frameCount);
+                const float frameFraction = glm::fract(currentFrame);
+                std::vector<AnimPose> poses = _avatarAnimSkeleton->getRelativeDefaultPoses();
+
+                // TODO: this needs more testing, it's possible that we need not only scale but also rotation and translation
+                // According to tests with unmatching avatar and animation armatures, sometimes bones are not rotated correctly.
+                // Matching armatures already work very well now.
+                const float UNIT_SCALE = _animationRig.GetScaleFactorGeometryToUnscaledRig() / _rig.GetScaleFactorGeometryToUnscaledRig();
+
+                for (int i = 0; i < animationJointNames.size(); i++) {
+                    const QString& name = animationJointNames[i];
+                    // As long as we need the model preRotations anyway, let's get the jointIndex from the bind skeleton rather than
+                    // trusting the .fst (which is sometimes not updated to match changes to .fbx).
+                    int mapping = _geometryResource->getHFMModel().getJointIndex(name);
+                    if (mapping != -1 && !_maskedJoints.contains(name)) {
+                        AnimPose floorPose = composeAnimPose(modelJoints[mapping], floorFrame.rotations[i],
+                                                             floorFrame.translations[i] * UNIT_SCALE);
+                        AnimPose ceilPose = composeAnimPose(modelJoints[mapping], ceilFrame.rotations[i],
+                                                            ceilFrame.translations[i] * UNIT_SCALE);
+                        blend(1, &floorPose, &ceilPose, frameFraction, &poses[mapping]);
+                    }
+                }
+
+                std::vector<AnimPose> absPoses = poses;
+                Q_ASSERT(_avatarAnimSkeleton != nullptr);
+                _avatarAnimSkeleton->convertRelativePosesToAbsolute(absPoses);
+                for (int i = 0; i < nJoints; i++) {
+                    JointData& data = _jointData[i];
+                    AnimPose& absPose = absPoses[i];
+                    if (data.rotation != absPose.rot()) {
+                        data.rotation = absPose.rot();
+                        data.rotationIsDefaultPose = false;
+                    }
+                    AnimPose& relPose = poses[i];
+                    if (data.translation != relPose.trans()) {
+                        data.translation = relPose.trans();
+                        data.translationIsDefaultPose = false;
+                    }
+                }
+
+            } else {
+                _animation.clear();
+            }
         }
     }
 
@@ -245,6 +273,7 @@ void ScriptableAvatar::setJointMappingsFromNetworkReply() {
         networkReply->deleteLater();
         return;
     }
+    // TODO: this works only with .fst files currently, not directly with FBX and GLB models
     {
         QWriteLocker writeLock(&_jointDataLock);
         QByteArray line;
@@ -253,7 +282,7 @@ void ScriptableAvatar::setJointMappingsFromNetworkReply() {
             if (line.startsWith("filename")) {
                 int filenameIndex = line.indexOf('=') + 1;
                 if (filenameIndex > 0) {
-                    _skeletonFBXURL = _skeletonModelURL.resolved(QString(line.mid(filenameIndex).trimmed()));
+                    _skeletonModelFilenameURL = _skeletonModelURL.resolved(QString(line.mid(filenameIndex).trimmed()));
                 }
             }
             if (!line.startsWith("jointIndex")) {
@@ -315,7 +344,9 @@ AvatarEntityMap ScriptableAvatar::getAvatarEntityDataInternal(bool allProperties
             EntityItemProperties properties = entity->getProperties(desiredProperties);
 
             QByteArray blob;
-            EntityItemProperties::propertiesToBlob(*_scriptEngine, sessionID, properties, blob, allProperties);
+            _helperScriptEngine.run( [&] {
+                EntityItemProperties::propertiesToBlob(*_helperScriptEngine.get(), sessionID, properties, blob, allProperties);
+            });
             data[id] = blob;
         }
     });
@@ -339,8 +370,12 @@ void ScriptableAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityDa
     while (dataItr != avatarEntityData.end()) {
         EntityItemProperties properties;
         const QByteArray& blob = dataItr.value();
-        if (!blob.isNull() && EntityItemProperties::blobToProperties(*_scriptEngine, blob, properties)) {
-            newProperties[dataItr.key()] = properties;
+        if (!blob.isNull()) {
+            _helperScriptEngine.run([&] {
+                if (EntityItemProperties::blobToProperties(*_helperScriptEngine.get(), blob, properties)) {
+                    newProperties[dataItr.key()] = properties;
+                }
+            });
         }
         ++dataItr;
     }
@@ -419,9 +454,16 @@ void ScriptableAvatar::updateAvatarEntity(const QUuid& entityID, const QByteArra
 
     EntityItemPointer entity;
     EntityItemProperties properties;
-    if (!EntityItemProperties::blobToProperties(*_scriptEngine, entityData, properties)) {
-        // entityData is corrupt
-        return;
+    {
+        // TODO: checking how often this happens and what is the performance impact of having the script engine on separate thread
+        // If it's happening often, a method to move HelperScriptEngine into the current thread would be a good idea
+        bool result = _helperScriptEngine.runWithResult<bool> ( [&]() {
+            return EntityItemProperties::blobToProperties(*_helperScriptEngine.get(), entityData, properties);
+        });
+        if (!result) {
+            // entityData is corrupt
+            return;
+        }
     }
 
     std::map<QUuid, EntityItemPointer>::iterator itr = _entities.find(entityID);
diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h
index 703a0a9f64..7e79d25cd0 100644
--- a/assignment-client/src/avatars/ScriptableAvatar.h
+++ b/assignment-client/src/avatars/ScriptableAvatar.h
@@ -19,6 +19,9 @@
 #include <AvatarData.h>
 #include <ScriptEngine.h>
 #include <EntityItem.h>
+#include "model-networking/ModelCache.h"
+#include "Rig.h"
+#include <HelperScriptEngine.h>
 
 /*@jsdoc
  * The <code>Avatar</code> API is used to manipulate scriptable avatars on the domain. This API is a subset of the 
@@ -217,12 +220,16 @@ private:
     AnimationPointer _animation;
     AnimationDetails _animationDetails;
     QStringList _maskedJoints;
-    AnimationPointer _bind; // a sleazy way to get the skeleton, given the various library/cmake dependencies
-    std::shared_ptr<AnimSkeleton> _animSkeleton;
+    GeometryResource::Pointer _geometryResource;
+    Rig _rig;
+    bool _isRigValid{false};
+    Rig _animationRig;
+    bool _isAnimationRigValid{false};
+    std::shared_ptr<AnimSkeleton> _avatarAnimSkeleton;
     QHash<QString, int> _fstJointIndices; ///< 1-based, since zero is returned for missing keys
     QStringList _fstJointNames; ///< in order of depth-first traversal
-    QUrl _skeletonFBXURL;
-    mutable ScriptEnginePointer _scriptEngine;
+    QUrl _skeletonModelFilenameURL; // This contains URL from filename field in fst file
+    mutable HelperScriptEngine _helperScriptEngine;
     std::map<QUuid, EntityItemPointer> _entities;
 
     /// Loads the joint indices, names from the FST file (if any)
diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp
index b16e4561d6..2b23434195 100644
--- a/assignment-client/src/scripts/EntityScriptServer.cpp
+++ b/assignment-client/src/scripts/EntityScriptServer.cpp
@@ -44,16 +44,8 @@
 using Mutex = std::mutex;
 using Lock = std::lock_guard<Mutex>;
 
-static std::mutex logBufferMutex;
-static std::string logBuffer;
-
 void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) {
     auto logMessage = LogHandler::getInstance().printMessage((LogMsgType) type, context, message);
-
-    if (!logMessage.isEmpty()) {
-        Lock lock(logBufferMutex);
-        logBuffer.append(logMessage.toStdString() + '\n');
-    }
 }
 
 int EntityScriptServer::_entitiesScriptEngineCount = 0;
@@ -217,10 +209,10 @@ void EntityScriptServer::handleEntityServerScriptLogPacket(QSharedPointer<Receiv
 }
 
 void EntityScriptServer::pushLogs() {
-    std::string buffer;
+    QJsonArray buffer;
     {
-        Lock lock(logBufferMutex);
-        std::swap(logBuffer, buffer);
+        Lock lock(_logBufferMutex);
+        std::swap(_logBuffer, buffer);
     }
 
     if (buffer.empty()) {
@@ -231,16 +223,27 @@ void EntityScriptServer::pushLogs() {
     }
 
     auto nodeList = DependencyManager::get<NodeList>();
+    QJsonDocument document;
+    document.setArray(buffer);
+    QString data(document.toJson());
+    std::string string = data.toStdString();
+    auto cstring = string.c_str();
     for (auto uuid : _logListeners) {
         auto node = nodeList->nodeWithUUID(uuid);
         if (node && node->getActiveSocket()) {
             auto packet = NLPacketList::create(PacketType::EntityServerScriptLog, QByteArray(), true, true);
-            packet->write(buffer.data(), buffer.size());
+            packet->write(cstring, strlen(cstring));
             nodeList->sendPacketList(std::move(packet), *node);
         }
     }
 }
 
+void EntityScriptServer::addLogEntry(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, ScriptMessage::Severity severity) {
+    ScriptMessage entry(message, fileName, lineNumber, entityID, ScriptMessage::ScriptType::TYPE_ENTITY_SCRIPT, severity);
+    Lock lock(_logBufferMutex);
+    _logBuffer.append(entry.toJson());
+}
+
 void EntityScriptServer::handleEntityScriptCallMethodPacket(QSharedPointer<ReceivedMessage> receivedMessage, SharedNodePointer senderNode) {
 
     if (_entitiesScriptManager && _entityViewer.getTree() && !_shuttingDown) {
@@ -469,6 +472,29 @@ void EntityScriptServer::resetEntitiesScriptEngine() {
     connect(newManager.get(), &ScriptManager::warningMessage, scriptEngines, &ScriptEngines::onWarningMessage);
     connect(newManager.get(), &ScriptManager::infoMessage, scriptEngines, &ScriptEngines::onInfoMessage);
 
+    // Make script engine messages available through ScriptDiscoveryService
+    connect(newManager.get(), &ScriptManager::infoEntityMessage, scriptEngines, &ScriptEngines::infoEntityMessage);
+    connect(newManager.get(), &ScriptManager::printedEntityMessage, scriptEngines, &ScriptEngines::printedEntityMessage);
+    connect(newManager.get(), &ScriptManager::errorEntityMessage, scriptEngines, &ScriptEngines::errorEntityMessage);
+    connect(newManager.get(), &ScriptManager::warningEntityMessage, scriptEngines, &ScriptEngines::warningEntityMessage);
+
+    connect(newManager.get(), &ScriptManager::infoEntityMessage,
+            [this](const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID) {
+                addLogEntry(message, fileName, lineNumber, entityID, ScriptMessage::Severity::SEVERITY_INFO);
+            });
+    connect(newManager.get(), &ScriptManager::printedEntityMessage,
+            [this](const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID) {
+                addLogEntry(message, fileName, lineNumber, entityID, ScriptMessage::Severity::SEVERITY_PRINT);
+            });
+    connect(newManager.get(), &ScriptManager::errorEntityMessage,
+            [this](const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID) {
+                addLogEntry(message, fileName, lineNumber, entityID, ScriptMessage::Severity::SEVERITY_ERROR);
+            });
+    connect(newManager.get(), &ScriptManager::warningEntityMessage,
+            [this](const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID) {
+                addLogEntry(message, fileName, lineNumber, entityID, ScriptMessage::Severity::SEVERITY_WARNING);
+            });
+
     connect(newManager.get(), &ScriptManager::update, this, [this] {
         _entityViewer.queryOctree();
         _entityViewer.getTree()->preUpdate();
diff --git a/assignment-client/src/scripts/EntityScriptServer.h b/assignment-client/src/scripts/EntityScriptServer.h
index 3f15f5733c..2fba985ef4 100644
--- a/assignment-client/src/scripts/EntityScriptServer.h
+++ b/assignment-client/src/scripts/EntityScriptServer.h
@@ -27,6 +27,8 @@
 #include <SimpleEntitySimulation.h>
 #include <ThreadedAssignment.h>
 #include <ScriptManager.h>
+#include <ScriptMessage.h>
+#include <QJsonArray>
 
 #include "../entities/EntityTreeHeadlessViewer.h"
 
@@ -55,10 +57,32 @@ private slots:
     void handleSettings();
     void updateEntityPPS();
 
+    /**
+         * @brief Handles log subscribe/unsubscribe requests
+         *
+         * Clients can subscribe to logs by sending a script log packet. Entity Script Server keeps list of subscribers
+         * and sends them logs in JSON format.
+         */
+
     void handleEntityServerScriptLogPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
 
+    /**
+         * @brief Transmit logs
+         *
+         * This is called periodically through a timer to transmit logs from scripts.
+         */
+
     void pushLogs();
 
+    /**
+         * @brief Adds log entry to the transmit buffer
+         *
+         * This is connected to entity script log events in the script manager and adds script log message to the buffer
+         * containing messages that will be sent to subscribed clients.
+         */
+
+    void addLogEntry(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, ScriptMessage::Severity severity);
+
     void handleEntityScriptCallMethodPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
 
 
@@ -85,6 +109,9 @@ private:
     EntityEditPacketSender _entityEditSender;
     EntityTreeHeadlessViewer _entityViewer;
 
+    QJsonArray _logBuffer;
+    std::mutex _logBufferMutex;
+
     int _maxEntityPPS { DEFAULT_MAX_ENTITY_PPS };
     int _entityPPSPerScript { DEFAULT_ENTITY_PPS_PER_SCRIPT };
 
diff --git a/cmake/compiler.cmake b/cmake/compiler.cmake
index 5108253403..316b0632c6 100644
--- a/cmake/compiler.cmake
+++ b/cmake/compiler.cmake
@@ -6,10 +6,6 @@ if (NOT "${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
   message( FATAL_ERROR "Only 64 bit builds supported." )
 endif()
 
-if (USE_CCACHE OR "$ENV{USE_CCACHE}")
-  configure_ccache()
-endif()
-
 if (WIN32)
   add_definitions(-DNOMINMAX -D_CRT_SECURE_NO_WARNINGS)
 
diff --git a/cmake/externals/LibOVR/CMakeLists.txt b/cmake/externals/LibOVR/CMakeLists.txt
index 51a85f0117..2ef232ae1f 100644
--- a/cmake/externals/LibOVR/CMakeLists.txt
+++ b/cmake/externals/LibOVR/CMakeLists.txt
@@ -15,14 +15,22 @@ string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
 
 if (WIN32)
 
+  # Note the -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
+  # It's important that we pass our build type down to other builds we make, especially on Windows.
+  # On Windows, debug libraries get a 'd' suffix, eg, LibOVRd.lib. This means that a mismatch of build
+  # types means we'll generate a LibOVRd.lib and the rest of the system will look for LibOVR.lib, or
+  # viceversa.
   ExternalProject_Add(
     ${EXTERNAL_NAME}
     URL "${EXTERNAL_BUILD_ASSETS}/dependencies/ovr_sdk_win_1.35.0.zip"
     URL_MD5 1e3e8b2101387af07ff9c841d0ea285e
-    CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>
+    CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
     PATCH_COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/LibOVRCMakeLists.txt" <SOURCE_DIR>/CMakeLists.txt
     LOG_DOWNLOAD 1
     DOWNLOAD_EXTRACT_TIMESTAMP 1
+    BUILD_BYPRODUCTS
+      "project/Lib/LibOVR.lib"
+      "project/Lib/LibOVRd.lib"
   )
 
   ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR)
diff --git a/cmake/externals/LibOVR/LibOVRCMakeLists.txt b/cmake/externals/LibOVR/LibOVRCMakeLists.txt
index a52cff5463..7740c618f6 100644
--- a/cmake/externals/LibOVR/LibOVRCMakeLists.txt
+++ b/cmake/externals/LibOVR/LibOVRCMakeLists.txt
@@ -1,6 +1,8 @@
-cmake_minimum_required(VERSION 3.2)
+cmake_minimum_required(VERSION 3.20)
 project(LibOVR)
 
+message(STATUS "Building LibOVR for ${CMAKE_BUILD_TYPE} configuration")
+
 include_directories(LibOVR/Include LibOVR/Src)
 file(GLOB HEADER_FILES LibOVR/Include/*.h)
 file(GLOB EXTRA_HEADER_FILES LibOVR/Include/Extras/*.h)  
diff --git a/cmake/externals/LibOVRPlatform/CMakeLists.txt b/cmake/externals/LibOVRPlatform/CMakeLists.txt
index 492827210a..d6b3478418 100644
--- a/cmake/externals/LibOVRPlatform/CMakeLists.txt
+++ b/cmake/externals/LibOVRPlatform/CMakeLists.txt
@@ -16,6 +16,8 @@ if (WIN32)
     INSTALL_COMMAND ""
     LOG_DOWNLOAD 1
     DOWNLOAD_EXTRACT_TIMESTAMP 1
+    BUILD_BYPRODUCTS
+    "project/src/LibOVRPlatform/Windows/LibOVRPlatform64_1.lib"
   )
 
   ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR)
diff --git a/cmake/externals/crashpad/CMakeLists.txt b/cmake/externals/crashpad/CMakeLists.txt
index c174022aa3..725c0172a5 100644
--- a/cmake/externals/crashpad/CMakeLists.txt
+++ b/cmake/externals/crashpad/CMakeLists.txt
@@ -13,6 +13,13 @@ if (WIN32)
       INSTALL_COMMAND ""
       LOG_DOWNLOAD 1
       DOWNLOAD_EXTRACT_TIMESTAMP 1
+      BUILD_BYPRODUCTS
+        "project/src/crashpad/out/Release_x64/lib_MD/crashpad_client.lib"
+        "project/src/crashpad/out/Release_x64/lib_MD/crashpad_util.lib"
+        "project/src/crashpad/out/Release_x64/lib_MD/base.lib"
+        "project/src/crashpad/out/Debug_x64/lib_MD/crashpad_client.lib"
+        "project/src/crashpad/out/Debug_x64/lib_MD/crashpad_util.lib"
+        "project/src/crashpad/out/Debug_x64/lib_MD/base.lib"
     )
 
     ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR)
diff --git a/cmake/externals/steamworks/CMakeLists.txt b/cmake/externals/steamworks/CMakeLists.txt
index 47e5029206..62961f46a4 100644
--- a/cmake/externals/steamworks/CMakeLists.txt
+++ b/cmake/externals/steamworks/CMakeLists.txt
@@ -4,18 +4,82 @@ set(EXTERNAL_NAME steamworks)
 
 string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
 
-set(STEAMWORKS_URL "${EXTERNAL_BUILD_ASSETS}/dependencies/steamworks_sdk_137.zip")
-set(STEAMWORKS_URL_MD5 "95ba9d0e3ddc04f8a8be17d2da806cbb")
+set(STEAMWORKS_URL "${EXTERNAL_BUILD_ASSETS}/dependencies/steamworks_sdk_158a.zip")
+set(STEAMWORKS_URL_SHA512 "fe906a7510a2125ab1441ad349e8bc31fafc9ab8130ec3843287e615a850305a8ed303e8d9e5bae4fee06024987834fb9f64c6c10d3da3784267a4906e59c831")
 
+# Ninja needs to know all the files that result from this upfront, so we need to tell it what files this is going
+# to generate with BUILD_BYPRODUCTS. We need to include all the files that are going to be referenced from elsewhere
+# in the build.
+#
+# This should include both libraries and headers, since from the point of view of the build, those are the outputs
+# of the project, even though we're not actually building anything here, and just unzipping an existing binary.
+#
+# I believe this list can't be obtained automatically from the compressed file, and needs to be generated by hand.
+# Steam SDK .zip has a sdk/ subdirectory, but for ExternalProject, this gets turned into project/src/steamworks.
+# So inside the SDK, sdk/redistributable_bin/steam_api.dll becomes project/src/steamworks/redistributable_bin/steam_api.dll
+# This can be seen under $BUILD_DIR/ext.
 ExternalProject_Add(
   ${EXTERNAL_NAME}
   URL ${STEAMWORKS_URL}
-  URL_MD5 ${STEAMWORKS_URL_MD5}
+  URL_HASH SHA512=${STEAMWORKS_URL_SHA512}
   CONFIGURE_COMMAND ""
   BUILD_COMMAND ""
   INSTALL_COMMAND ""
   LOG_DOWNLOAD 1
   DOWNLOAD_EXTRACT_TIMESTAMP 1
+  BUILD_BYPRODUCTS
+    "project/src/steamworks/redistributable_bin/win64/steam_api64.lib"
+    "project/src/steamworks/redistributable_bin/win64/steam_api64.dll"
+    "project/src/steamworks/redistributable_bin/osx/steam_api.dylib"
+    "project/src/steamworks/redistributable_bin/linux64/libsteam_api.so"
+    "project/src/steamworks/redistributable_bin/linux32/libsteam_api.so"
+    "project/src/steamworks/redistributable_bin/steam_api.lib"
+    "project/src/steamworks/redistributable_bin/steam_api.dll"
+    "project/src/steamworks/public/steam/isteamapplist.h"
+    "project/src/steamworks/public/steam/isteamapps.h"
+    "project/src/steamworks/public/steam/isteamappticket.h"
+    "project/src/steamworks/public/steam/isteamclient.h"
+    "project/src/steamworks/public/steam/isteamcontroller.h"
+    "project/src/steamworks/public/steam/isteamdualsense.h"
+    "project/src/steamworks/public/steam/isteamfriends.h"
+    "project/src/steamworks/public/steam/isteamgamecoordinator.h"
+    "project/src/steamworks/public/steam/isteamgameserver.h"
+    "project/src/steamworks/public/steam/isteamgameserverstats.h"
+    "project/src/steamworks/public/steam/isteamhtmlsurface.h"
+    "project/src/steamworks/public/steam/isteamhttp.h"
+    "project/src/steamworks/public/steam/isteaminput.h"
+    "project/src/steamworks/public/steam/isteaminventory.h"
+    "project/src/steamworks/public/steam/isteammatchmaking.h"
+    "project/src/steamworks/public/steam/isteammusic.h"
+    "project/src/steamworks/public/steam/isteammusicremote.h"
+    "project/src/steamworks/public/steam/isteamnetworking.h"
+    "project/src/steamworks/public/steam/isteamnetworkingmessages.h"
+    "project/src/steamworks/public/steam/isteamnetworkingsockets.h"
+    "project/src/steamworks/public/steam/isteamnetworkingutils.h"
+    "project/src/steamworks/public/steam/isteamparentalsettings.h"
+    "project/src/steamworks/public/steam/isteamps3overlayrenderer.h"
+    "project/src/steamworks/public/steam/isteamremoteplay.h"
+    "project/src/steamworks/public/steam/isteamremotestorage.h"
+    "project/src/steamworks/public/steam/isteamscreenshots.h"
+    "project/src/steamworks/public/steam/isteamugc.h"
+    "project/src/steamworks/public/steam/isteamuser.h"
+    "project/src/steamworks/public/steam/isteamuserstats.h"
+    "project/src/steamworks/public/steam/isteamutils.h"
+    "project/src/steamworks/public/steam/isteamvideo.h"
+    "project/src/steamworks/public/steam/matchmakingtypes.h"
+    "project/src/steamworks/public/steam/steam_api_common.h"
+    "project/src/steamworks/public/steam/steam_api_flat.h"
+    "project/src/steamworks/public/steam/steam_api.h"
+    "project/src/steamworks/public/steam/steam_api_internal.h"
+    "project/src/steamworks/public/steam/steamclientpublic.h"
+    "project/src/steamworks/public/steam/steamencryptedappticket.h"
+    "project/src/steamworks/public/steam/steam_gameserver.h"
+    "project/src/steamworks/public/steam/steamhttpenums.h"
+    "project/src/steamworks/public/steam/steamnetworkingfakeip.h"
+    "project/src/steamworks/public/steam/steamnetworkingtypes.h"
+    "project/src/steamworks/public/steam/steamps3params.h"
+    "project/src/steamworks/public/steam/steamtypes.h"
+    "project/src/steamworks/public/steam/steamuniverse.h"
 )
 
 set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals")
diff --git a/cmake/macros/ConfigureCCache.cmake b/cmake/macros/ConfigureCCache.cmake
deleted file mode 100644
index bec159ef09..0000000000
--- a/cmake/macros/ConfigureCCache.cmake
+++ /dev/null
@@ -1,45 +0,0 @@
-#
-#  ConfigureCCache.cmake
-#  cmake/macros
-#
-#  Created by Clement Brisset on 10/10/18.
-#  Copyright 2018 High Fidelity, Inc.
-#
-#  Distributed under the Apache License, Version 2.0.
-#  See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html
-#
-
-macro(configure_ccache)
-  find_program(CCACHE_PROGRAM ccache)
-  if(CCACHE_PROGRAM)
-    message(STATUS "Configuring ccache")
-
-    # Set up wrapper scripts
-    set(C_LAUNCHER   "${CCACHE_PROGRAM}")
-    set(CXX_LAUNCHER "${CCACHE_PROGRAM}")
-    
-    set(LAUNCH_C_IN "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/launch-c.in")
-    set(LAUNCH_CXX_IN "${CMAKE_CURRENT_SOURCE_DIR}/cmake/templates/launch-cxx.in")
-    set(LAUNCH_C "${CMAKE_BINARY_DIR}/CMakeFiles/launch-c")
-    set(LAUNCH_CXX "${CMAKE_BINARY_DIR}/CMakeFiles/launch-cxx")
-
-    configure_file(${LAUNCH_C_IN} ${LAUNCH_C})
-    configure_file(${LAUNCH_CXX_IN} ${LAUNCH_CXX})
-    execute_process(COMMAND chmod a+rx ${LAUNCH_C} ${LAUNCH_CXX})
-
-    if(CMAKE_GENERATOR STREQUAL "Xcode")
-      # Set Xcode project attributes to route compilation and linking
-      # through our scripts
-      set(CMAKE_XCODE_ATTRIBUTE_CC ${LAUNCH_C})
-      set(CMAKE_XCODE_ATTRIBUTE_CXX ${LAUNCH_CXX})
-      set(CMAKE_XCODE_ATTRIBUTE_LD ${LAUNCH_C})
-      set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS ${LAUNCH_CXX})
-    else()
-      # Support Unix Makefiles and Ninja
-      set(CMAKE_C_COMPILER_LAUNCHER ${LAUNCH_C})
-      set(CMAKE_CXX_COMPILER_LAUNCHER ${LAUNCH_CXX})
-    endif()
-  else()
-    message(WARNING "Could not find ccache")
-  endif()
-endmacro()
diff --git a/cmake/macros/TargetOpenEXR.cmake b/cmake/macros/TargetOpenEXR.cmake
index 9d63ba3ef4..29a1cd77a0 100644
--- a/cmake/macros/TargetOpenEXR.cmake
+++ b/cmake/macros/TargetOpenEXR.cmake
@@ -14,36 +14,38 @@ macro(TARGET_OPENEXR)
                 TMP
                 REGEX "#define OPENEXR_VERSION_STRING.*$")
             string(REGEX MATCHALL "[0-9.]+" OPENEXR_VERSION ${TMP})
-    
+
             file(STRINGS
                 ${openexr_config_file}
                 TMP
                 REGEX "#define OPENEXR_VERSION_MAJOR.*$")
             string(REGEX MATCHALL "[0-9]" OPENEXR_MAJOR_VERSION ${TMP})
-    
+
             file(STRINGS
                 ${openexr_config_file}
                 TMP
                 REGEX "#define OPENEXR_VERSION_MINOR.*$")
             string(REGEX MATCHALL "[0-9]" OPENEXR_MINOR_VERSION ${TMP})
+        else()
+            message(WARNING "Failed to find ${openexr_config_file}")
         endif()
 
         set(OPENEXR_LIBRARY_RELEASE "")
         set(OPENEXR_LIBRARY_DEBUG "")
         foreach(OPENEXR_LIB
-            IlmImf
-            IlmImfUtil
-            Half
+            OpenEXRCore
+            OpenEXR
+            OpenEXRUtil
             Iex
-            IexMath
+            IlmThread
             Imath
-            IlmThread)
+            )
 
             # OpenEXR libraries may be suffixed with the version number, so we search
             # using both versioned and unversioned names.
             find_library(OPENEXR_${OPENEXR_LIB}_LIBRARY_RELEASE
                 NAMES
-                    ${OPENEXR_LIB}-${OPENEXR_MAJOR_VERSION}_${OPENEXR_MINOR_VERSION}_s
+                    ${OPENEXR_LIB}-${OPENEXR_MAJOR_VERSION}_${OPENEXR_MINOR_VERSION}
                     ${OPENEXR_LIB}_s
 
                 PATHS ${VCPKG_INSTALL_ROOT}/lib NO_DEFAULT_PATH
@@ -52,13 +54,15 @@ macro(TARGET_OPENEXR)
 
             if(OPENEXR_${OPENEXR_LIB}_LIBRARY_RELEASE)
                 list(APPEND OPENEXR_LIBRARY_RELEASE ${OPENEXR_${OPENEXR_LIB}_LIBRARY_RELEASE})
+            else()
+                message(WARNING "Failed to find ${OPENEXR_LIB} (release); ${OPENEXR_LIB}-${OPENEXR_MAJOR_VERSION}_${OPENEXR_MINOR_VERSION}")
             endif()
 
             # OpenEXR libraries may be suffixed with the version number, so we search
             # using both versioned and unversioned names.
             find_library(OPENEXR_${OPENEXR_LIB}_LIBRARY_DEBUG
                 NAMES
-                    ${OPENEXR_LIB}-${OPENEXR_MAJOR_VERSION}_${OPENEXR_MINOR_VERSION}_s_d
+                    ${OPENEXR_LIB}-${OPENEXR_MAJOR_VERSION}_${OPENEXR_MINOR_VERSION}_d
                     ${OPENEXR_LIB}_s_d
 
                 PATHS ${VCPKG_INSTALL_ROOT}/debug/lib NO_DEFAULT_PATH
@@ -67,10 +71,19 @@ macro(TARGET_OPENEXR)
 
             if(OPENEXR_${OPENEXR_LIB}_LIBRARY_DEBUG)
                 list(APPEND OPENEXR_LIBRARY_DEBUG ${OPENEXR_${OPENEXR_LIB}_LIBRARY_DEBUG})
+            else()
+                message(WARNING "Failed to find ${OPENEXR_LIB} (debug); ${OPENEXR_LIB}-${OPENEXR_MAJOR_VERSION}_${OPENEXR_MINOR_VERSION}_d")
             endif()
         endforeach(OPENEXR_LIB)
 
         select_library_configurations(OPENEXR)
         target_link_libraries(${TARGET_NAME} ${OPENEXR_LIBRARY})
+        target_include_directories(${TARGET_NAME} PUBLIC "${VCPKG_INSTALL_ROOT}/include/Imath")
+
+        # This prevents:
+        # LNK2001	unresolved external symbol imath_half_to_float_table
+        #
+        # Apparently something changed in newer versions.
+        target_compile_definitions(${TARGET_NAME} PUBLIC IMATH_HALF_NO_LOOKUP_TABLE)
     endif()
 endmacro()
diff --git a/cmake/ports/artery-font-format/disable-checksum.patch b/cmake/ports/artery-font-format/disable-checksum.patch
new file mode 100644
index 0000000000..cd5133ea9e
--- /dev/null
+++ b/cmake/ports/artery-font-format/disable-checksum.patch
@@ -0,0 +1,38 @@
+diff --git a/artery-font/serialization.hpp b/artery-font/serialization.hpp
+index 69263a8..6075eda 100644
+--- a/artery-font/serialization.hpp
++++ b/artery-font/serialization.hpp
+@@ -109,15 +109,16 @@ template <ReadFunction READ, typename REAL, template <typename> class LIST, clas
+ bool decode(ArteryFont<REAL, LIST, BYTE_ARRAY, STRING> &font, void *userData) {
+     uint32 totalLength = 0;
+     uint32 prevLength = 0;
+-    uint32 checksum = crc32Init();
++    //uint32 checksum = crc32Init();
+     byte dump[4];
+     #define ARTERY_FONT_DECODE_READ(target, len) { \
+         if (READ((target), (len), userData) != int(len)) \
+             return false; \
+         totalLength += (len); \
+-        for (int _i = 0; _i < int(len); ++_i) \
+-            checksum = crc32Update(checksum, reinterpret_cast<const byte *>(target)[_i]); \
+     }
++    //    for (int _i = 0; _i < int(len); ++_i) \
++    //        checksum = crc32update(checksum, reinterpret_cast<const byte *>(target)[_i]); \
++    //}
+     #define ARTERY_FONT_DECODE_REALIGN() { \
+         if (totalLength&0x03u) { \
+             uint32 len = 0x04u-(totalLength&0x03u); \
+@@ -228,10 +229,10 @@ bool decode(ArteryFont<REAL, LIST, BYTE_ARRAY, STRING> &font, void *userData) {
+         ARTERY_FONT_DECODE_READ(&footer, sizeof(footer)-sizeof(footer.checksum));
+         if (footer.magicNo != ARTERY_FONT_FOOTER_MAGIC_NO)
+             return false;
+-        uint32 finalChecksum = checksum;
++        //uint32 finalChecksum = checksum;
+         ARTERY_FONT_DECODE_READ(&footer.checksum, sizeof(footer.checksum));
+-        if (footer.checksum != finalChecksum)
+-            return false;
++        //if (footer.checksum != finalChecksum)
++        //    return false;
+         if (totalLength != footer.totalLength)
+             return false;
+     }
diff --git a/cmake/ports/artery-font-format/portfile.cmake b/cmake/ports/artery-font-format/portfile.cmake
new file mode 100644
index 0000000000..4b2491ab67
--- /dev/null
+++ b/cmake/ports/artery-font-format/portfile.cmake
@@ -0,0 +1,15 @@
+# header-only library
+
+vcpkg_from_github(
+        OUT_SOURCE_PATH SOURCE_PATH
+        REPO Chlumsky/artery-font-format
+        REF 34134bde3cea35a93c2ae5703fa8d3d463793400
+        SHA512 6b2fc0de9ca7b367c9b98f829ce6cee858f1252b12a49b6f1e89a5a2fdb109e20ef812f0b30495195ca0b177adae32d5e238fdc305724857ced098be2d29a6af
+        HEAD_REF master
+        PATCHES "disable-checksum.patch"
+)
+
+file(COPY "${SOURCE_PATH}/artery-font" DESTINATION "${CURRENT_PACKAGES_DIR}/include")
+
+# Handle copyright
+configure_file("${SOURCE_PATH}/LICENSE.txt" "${CURRENT_PACKAGES_DIR}/share/${PORT}/copyright" COPYONLY)
\ No newline at end of file
diff --git a/cmake/ports/artery-font-format/vcpkg.json b/cmake/ports/artery-font-format/vcpkg.json
new file mode 100644
index 0000000000..9bfcddcb24
--- /dev/null
+++ b/cmake/ports/artery-font-format/vcpkg.json
@@ -0,0 +1,7 @@
+{
+  "name": "artery-font-format",
+  "version": "1.0.1",
+  "description": "Header-only C++ library that facilitates encoding and decoding of the Artery Atlas Font format",
+  "homepage": "https://github.com/Chlumsky/artery-font-format",
+  "license": "MIT"
+}
\ No newline at end of file
diff --git a/cmake/ports/cgltf/portfile.cmake b/cmake/ports/cgltf/portfile.cmake
new file mode 100644
index 0000000000..95cc008a20
--- /dev/null
+++ b/cmake/ports/cgltf/portfile.cmake
@@ -0,0 +1,15 @@
+# header-only library
+
+vcpkg_from_github(
+        OUT_SOURCE_PATH SOURCE_PATH
+        REPO jkuhlmann/cgltf
+        REF de399881c65c438a635627c749440eeea7e05599
+        SHA512 753923116b92642848ff2bda70695ddd0e7be6db43ed3cfc37aff4cba90a29a92e3dbda139a5f2c80cad1d2cdaf81e1383e4ea7a12195f61fe8cfeb105e53ea2
+        HEAD_REF master
+)
+
+file(COPY "${SOURCE_PATH}/cgltf.h" DESTINATION "${CURRENT_PACKAGES_DIR}/include")
+file(COPY "${SOURCE_PATH}/cgltf_write.h" DESTINATION "${CURRENT_PACKAGES_DIR}/include")
+
+# Handle copyright
+configure_file("${SOURCE_PATH}/LICENSE" "${CURRENT_PACKAGES_DIR}/share/${PORT}/copyright" COPYONLY)
\ No newline at end of file
diff --git a/cmake/ports/cgltf/vcpkg.json b/cmake/ports/cgltf/vcpkg.json
new file mode 100644
index 0000000000..a57db71cb4
--- /dev/null
+++ b/cmake/ports/cgltf/vcpkg.json
@@ -0,0 +1,7 @@
+{
+  "name": "cgltf",
+  "version": "1.13",
+  "description": "Single-file glTF 2.0 loader and writer written in C99",
+  "homepage": "https://github.com/jkuhlmann/cgltf",
+  "license": "MIT"
+}
\ No newline at end of file
diff --git a/cmake/ports/hifi-deps/CONTROL b/cmake/ports/hifi-deps/CONTROL
index ee9f4cf1b3..a904618ee6 100644
--- a/cmake/ports/hifi-deps/CONTROL
+++ b/cmake/ports/hifi-deps/CONTROL
@@ -5,4 +5,4 @@
 Source: hifi-deps
 Version: 0.1.5-github-actions
 Description: Collected dependencies for High Fidelity applications
-Build-Depends: bullet3, draco, etc2comp, glad, glm, node, nvtt, openexr (!android), openssl (windows), opus, polyvox, tbb (!android), vhacd, webrtc (!android|!(linux&arm)), zlib
+Build-Depends: artery-font-format, bullet3, cgltf, draco, etc2comp, glad, glm, node, nvtt, openexr (!android), openssl (windows), opus, polyvox, tbb (!android), vhacd, webrtc (!android|!(linux&arm)), zlib
diff --git a/cmake/ports/imath/portfile.cmake b/cmake/ports/imath/portfile.cmake
new file mode 100644
index 0000000000..3f4f34ad58
--- /dev/null
+++ b/cmake/ports/imath/portfile.cmake
@@ -0,0 +1,25 @@
+vcpkg_from_github(
+    OUT_SOURCE_PATH SOURCE_PATH
+    REPO AcademySoftwareFoundation/Imath
+    REF v3.1.9
+    SHA512 ad96b2ac306fc13c01e8ea3256f885499c3f545be327feaba0f5e093b70b544bcca6f8b353fa7e35107aae515c19caced44331a95d0414f367ead4691ec73564
+    HEAD_REF master
+)
+
+vcpkg_cmake_configure(
+    SOURCE_PATH "${SOURCE_PATH}"
+    OPTIONS
+        -DIMATH_INSTALL_SYM_LINK=OFF
+        -DBUILD_TESTING=OFF
+        -DIMATH_INSTALL_PKG_CONFIG=ON
+)
+
+vcpkg_cmake_install()
+
+vcpkg_copy_pdbs()
+vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/Imath)
+vcpkg_fixup_pkgconfig()
+
+file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include")
+
+file(INSTALL "${SOURCE_PATH}/LICENSE.md" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" RENAME copyright)
diff --git a/cmake/ports/imath/vcpkg.json b/cmake/ports/imath/vcpkg.json
new file mode 100644
index 0000000000..85b1cb6915
--- /dev/null
+++ b/cmake/ports/imath/vcpkg.json
@@ -0,0 +1,18 @@
+{
+  "name": "imath",
+  "version": "3.1.9",
+  "port-version": 1,
+  "description": "Imath is a C++ and Python library of 2D and 3D vector, matrix, and math operations for computer graphics.",
+  "homepage": "https://github.com/AcademySoftwareFoundation/Imath",
+  "license": "BSD-3-Clause",
+  "dependencies": [
+    {
+      "name": "vcpkg-cmake",
+      "host": true
+    },
+    {
+      "name": "vcpkg-cmake-config",
+      "host": true
+    }
+  ]
+}
diff --git a/cmake/ports/node/portfile.cmake b/cmake/ports/node/portfile.cmake
old mode 100755
new mode 100644
index 5407a0d276..8a33927a78
--- a/cmake/ports/node/portfile.cmake
+++ b/cmake/ports/node/portfile.cmake
@@ -1,4 +1,4 @@
-# Copyright 2023 Overte e.V.
+# Copyright 2023-2024 Overte e.V.
 # SPDX-License-Identifier: Apache-2.0
 
 set(NODE_VERSION 18.14.2)
@@ -28,9 +28,9 @@ else ()
     vcpkg_from_github(
         OUT_SOURCE_PATH SOURCE_PATH
         REPO nodejs/node
-        REF v18.16.1
-        SHA512 cd2d7871a1a2aca8d800e0a501bd2836cbce076de750dcfc0b2bbe602c8a23705154bfb12faa3ff78e25ec753f419220742228569c281fa458987fb24f6d4d09
-        HEAD_REF v18.16.1
+        REF v18.20.2
+        SHA512 10d3637c26274677d137f76bbb648d0e7851c994634a16c89858c3a13094a0692ea2cb9a787c6463c3001abd71dab0d83123127bc305171d097c48d21d691678
+        HEAD_REF v18.20.2
     )
     # node cannot configure out of source, which VCPKG expects. So we copy the source to the configure directory.
     file(COPY ${SOURCE_PATH}/ DESTINATION "${CURRENT_BUILDTREES_DIR}")
diff --git a/cmake/ports/openexr/CONTROL b/cmake/ports/openexr/CONTROL
deleted file mode 100644
index d59ab286e1..0000000000
--- a/cmake/ports/openexr/CONTROL
+++ /dev/null
@@ -1,4 +0,0 @@
-Source: openexr
-Version: 2.3.0-2
-Description: OpenEXR is a high dynamic-range (HDR) image file format developed by Industrial Light & Magic for use in computer imaging applications
-Build-Depends: zlib
\ No newline at end of file
diff --git a/cmake/ports/openexr/FindOpenEXR.cmake b/cmake/ports/openexr/FindOpenEXR.cmake
deleted file mode 100644
index a381c6db9a..0000000000
--- a/cmake/ports/openexr/FindOpenEXR.cmake
+++ /dev/null
@@ -1,87 +0,0 @@
-include(FindPackageHandleStandardArgs)
-
-find_path(OpenEXR_INCLUDE_DIRS OpenEXR/OpenEXRConfig.h)
-find_path(OPENEXR_INCLUDE_PATHS NAMES ImfRgbaFile.h PATH_SUFFIXES OpenEXR)
-
-file(STRINGS "${OpenEXR_INCLUDE_DIRS}/OpenEXR/OpenEXRConfig.h" OPENEXR_CONFIG_H)
-
-string(REGEX REPLACE "^.*define OPENEXR_VERSION_MAJOR ([0-9]+).*$" "\\1" OpenEXR_VERSION_MAJOR "${OPENEXR_CONFIG_H}")
-string(REGEX REPLACE "^.*define OPENEXR_VERSION_MINOR ([0-9]+).*$" "\\1" OpenEXR_VERSION_MINOR "${OPENEXR_CONFIG_H}")
-set(OpenEXR_LIB_SUFFIX "${OpenEXR_VERSION_MAJOR}_${OpenEXR_VERSION_MINOR}")
-
-include(SelectLibraryConfigurations)
-
-if(NOT OpenEXR_BASE_LIBRARY)
-  find_library(OpenEXR_BASE_LIBRARY_RELEASE NAMES IlmImf-${OpenEXR_LIB_SUFFIX})
-  find_library(OpenEXR_BASE_LIBRARY_DEBUG NAMES IlmImf-${OpenEXR_LIB_SUFFIX}_d)
-  select_library_configurations(OpenEXR_BASE)
-endif()
-
-if(NOT OpenEXR_UTIL_LIBRARY)
-  find_library(OpenEXR_UTIL_LIBRARY_RELEASE NAMES IlmImfUtil-${OpenEXR_LIB_SUFFIX})
-  find_library(OpenEXR_UTIL_LIBRARY_DEBUG NAMES IlmImfUtil-${OpenEXR_LIB_SUFFIX}_d)
-  select_library_configurations(OpenEXR_UTIL)
-endif()
-
-if(NOT OpenEXR_HALF_LIBRARY)
-  find_library(OpenEXR_HALF_LIBRARY_RELEASE NAMES Half-${OpenEXR_LIB_SUFFIX})
-  find_library(OpenEXR_HALF_LIBRARY_DEBUG NAMES Half-${OpenEXR_LIB_SUFFIX}_d)
-  select_library_configurations(OpenEXR_HALF)
-endif()
-
-if(NOT OpenEXR_IEX_LIBRARY)
-  find_library(OpenEXR_IEX_LIBRARY_RELEASE NAMES Iex-${OpenEXR_LIB_SUFFIX})
-  find_library(OpenEXR_IEX_LIBRARY_DEBUG NAMES Iex-${OpenEXR_LIB_SUFFIX}_d)
-  select_library_configurations(OpenEXR_IEX)
-endif()
-
-if(NOT OpenEXR_MATH_LIBRARY)
-  find_library(OpenEXR_MATH_LIBRARY_RELEASE NAMES Imath-${OpenEXR_LIB_SUFFIX})
-  find_library(OpenEXR_MATH_LIBRARY_DEBUG NAMES Imath-${OpenEXR_LIB_SUFFIX}_d)
-  select_library_configurations(OpenEXR_MATH)
-endif()
-
-if(NOT OpenEXR_THREAD_LIBRARY)
-  find_library(OpenEXR_THREAD_LIBRARY_RELEASE NAMES IlmThread-${OpenEXR_LIB_SUFFIX})
-  find_library(OpenEXR_THREAD_LIBRARY_DEBUG NAMES IlmThread-${OpenEXR_LIB_SUFFIX}_d)
-  select_library_configurations(OpenEXR_THREAD)
-endif()
-
-if(NOT OpenEXR_IEXMATH_LIBRARY)
-  find_library(OpenEXR_IEXMATH_LIBRARY_RELEASE NAMES IexMath-${OpenEXR_LIB_SUFFIX})
-  find_library(OpenEXR_IEXMATH_LIBRARY_DEBUG NAMES IexMath-${OpenEXR_LIB_SUFFIX}d)
-  select_library_configurations(OpenEXR_IEXMATH)
-endif()
-
-set(OPENEXR_HALF_LIBRARY "${OpenEXR_HALF_LIBRARY}")
-set(OPENEXR_IEX_LIBRARY "${OpenEXR_IEX_LIBRARY}")
-set(OPENEXR_IMATH_LIBRARY "${OpenEXR_MATH_LIBRARY}")
-set(OPENEXR_ILMIMF_LIBRARY "${OpenEXR_BASE_LIBRARY}")
-set(OPENEXR_ILMIMFUTIL_LIBRARY "${OpenEXR_UTIL_LIBRARY}")
-set(OPENEXR_ILMTHREAD_LIBRARY "${OpenEXR_THREAD_LIBRARY}")
-
-set(OpenEXR_LIBRARY "${OpenEXR_BASE_LIBRARY}")
-
-set(OpenEXR_LIBRARIES
-    ${OpenEXR_LIBRARY}
-    ${OpenEXR_MATH_LIBRARY}
-    ${OpenEXR_IEXMATH_LIBRARY}
-    ${OpenEXR_UTIL_LIBRARY}
-    ${OpenEXR_HALF_LIBRARY}
-    ${OpenEXR_IEX_LIBRARY}
-    ${OpenEXR_THREAD_LIBRARY}
-)
-
-set(OPENEXR_LIBRARIES
-    ${OPENEXR_HALF_LIBRARY}
-    ${OPENEXR_IEX_LIBRARY}
-    ${OPENEXR_IMATH_LIBRARY}
-    ${OPENEXR_ILMIMF_LIBRARY}
-    ${OPENEXR_ILMTHREAD_LIBRARY}
-)
-
-FIND_PACKAGE_HANDLE_STANDARD_ARGS(OpenEXR REQUIRED_VARS OpenEXR_LIBRARIES OpenEXR_INCLUDE_DIRS)
-
-if(OpenEXR_FOUND)
-    set(OPENEXR_FOUND 1)
-endif()
diff --git a/cmake/ports/openexr/fix-arm64-windows-build.patch b/cmake/ports/openexr/fix-arm64-windows-build.patch
new file mode 100644
index 0000000000..1d3310a8b9
--- /dev/null
+++ b/cmake/ports/openexr/fix-arm64-windows-build.patch
@@ -0,0 +1,13 @@
+diff --git a/src/lib/OpenEXRCore/internal_dwa_simd.h b/src/lib/OpenEXRCore/internal_dwa_simd.h
+index 7b53501ac..ca69c9848 100644
+--- a/src/lib/OpenEXRCore/internal_dwa_simd.h
++++ b/src/lib/OpenEXRCore/internal_dwa_simd.h
+@@ -18,7 +18,7 @@
+ // aligned. Unaligned pointers may risk seg-faulting.
+ //
+ 
+-#if defined __SSE2__ || (_MSC_VER >= 1300 && !_M_CEE_PURE)
++#if defined __SSE2__ || (_MSC_VER >= 1300 && (_M_IX86 || _M_X64) && !_M_CEE_PURE)
+ #    define IMF_HAVE_SSE2 1
+ #    include <emmintrin.h>
+ #    include <mmintrin.h>
diff --git a/cmake/ports/openexr/fix_install_ilmimf.patch b/cmake/ports/openexr/fix_install_ilmimf.patch
deleted file mode 100644
index db65be7368..0000000000
--- a/cmake/ports/openexr/fix_install_ilmimf.patch
+++ /dev/null
@@ -1,19 +0,0 @@
-diff --git a/OpenEXR/IlmImf/CMakeLists.txt b/OpenEXR/IlmImf/CMakeLists.txt
-index e1a8740..d31cf68 100644
---- a/OpenEXR/IlmImf/CMakeLists.txt
-+++ b/OpenEXR/IlmImf/CMakeLists.txt
-@@ -2,14 +2,6 @@
- 
- SET(CMAKE_INCLUDE_CURRENT_DIR 1)
- 
--IF (WIN32)
--  SET(RUNTIME_DIR ${OPENEXR_PACKAGE_PREFIX}/bin)
--  SET(WORKING_DIR ${RUNTIME_DIR})
--ELSE ()
--  SET(RUNTIME_DIR ${OPENEXR_PACKAGE_PREFIX}/lib)
--  SET(WORKING_DIR .)
--ENDIF ()
--
- SET(BUILD_B44EXPLOGTABLE OFF)
- IF (NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/b44ExpLogTable.h")
-   SET(BUILD_B44EXPLOGTABLE ON)
diff --git a/cmake/ports/openexr/portfile.cmake b/cmake/ports/openexr/portfile.cmake
index 6e773434e8..8ffa6c76bb 100644
--- a/cmake/ports/openexr/portfile.cmake
+++ b/cmake/ports/openexr/portfile.cmake
@@ -1,71 +1,46 @@
-set(OPENEXR_VERSION 2.3.0)
-set(OPENEXR_HASH 268ae64b40d21d662f405fba97c307dad1456b7d996a447aadafd41b640ca736d4851d9544b4741a94e7b7c335fe6e9d3b16180e710671abfc0c8b2740b147b2)
-
 vcpkg_from_github(
-  OUT_SOURCE_PATH SOURCE_PATH
-  REPO openexr/openexr
-  REF v${OPENEXR_VERSION}
-  SHA512 ${OPENEXR_HASH}
-  HEAD_REF master
-  PATCHES "fix_install_ilmimf.patch"
+    OUT_SOURCE_PATH SOURCE_PATH
+    REPO AcademySoftwareFoundation/openexr
+    REF "v${VERSION}"
+    SHA512 ec60e79341695452e05f50bbcc0d55e0ce00fbb64cdec01a83911189c8643eb28a8046b14ee4230e5f438f018f2f1d0714f691983474d7979befd199f3f34758
+    HEAD_REF master
+    PATCHES
+        fix-arm64-windows-build.patch # https://github.com/AcademySoftwareFoundation/openexr/pull/1447
 )
 
-set(OPENEXR_STATIC ON)
-set(OPENEXR_SHARED OFF)
-
-vcpkg_configure_cmake(SOURCE_PATH ${SOURCE_PATH}
-  PREFER_NINJA
-  OPTIONS
-    -DOPENEXR_BUILD_PYTHON_LIBS=OFF
-    -DOPENEXR_BUILD_VIEWERS=OFF
-    -DOPENEXR_RUN_FUZZ_TESTS=OFF
-    -DOPENEXR_BUILD_SHARED=${OPENEXR_SHARED}
-    -DOPENEXR_BUILD_STATIC=${OPENEXR_STATIC}
-  OPTIONS_DEBUG
-    -DILMBASE_PACKAGE_PREFIX=${CURRENT_INSTALLED_DIR}/debug
-  OPTIONS_RELEASE
-    -DILMBASE_PACKAGE_PREFIX=${CURRENT_INSTALLED_DIR})
-
-vcpkg_install_cmake()
-
-file(REMOVE_RECURSE ${CURRENT_PACKAGES_DIR}/debug/include)
-file(REMOVE_RECURSE ${CURRENT_PACKAGES_DIR}/debug/share)
-
-# NOTE: Only use ".exe" extension on Windows executables.
-# Is there a cleaner way to do this?
-if(WIN32)
-    set(EXECUTABLE_SUFFIX ".exe")
-else()
-    set(EXECUTABLE_SUFFIX "")
-endif()
-file(REMOVE ${CURRENT_PACKAGES_DIR}/debug/bin/exrenvmap${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/debug/bin/exrheader${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/debug/bin/exrmakepreview${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/debug/bin/exrmaketiled${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/debug/bin/exrmultipart${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/debug/bin/exrmultiview${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/debug/bin/exrstdattr${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/bin/exrenvmap${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/bin/exrheader${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/bin/exrmakepreview${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/bin/exrmaketiled${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/bin/exrmultipart${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/bin/exrmultiview${EXECUTABLE_SUFFIX})
-file(REMOVE ${CURRENT_PACKAGES_DIR}/bin/exrstdattr${EXECUTABLE_SUFFIX})
-
+vcpkg_check_features(OUT_FEATURE_OPTIONS OPTIONS
+    FEATURES
+        tools   OPENEXR_BUILD_TOOLS
+        tools   OPENEXR_INSTALL_TOOLS
+)
+vcpkg_cmake_configure(
+    SOURCE_PATH "${SOURCE_PATH}"
+    OPTIONS
+        ${OPTIONS}
+        -DBUILD_TESTING=OFF
+        -DOPENEXR_INSTALL_EXAMPLES=OFF
+        -DBUILD_DOCS=OFF
+    OPTIONS_DEBUG
+        -DOPENEXR_BUILD_TOOLS=OFF
+        -DOPENEXR_INSTALL_TOOLS=OFF
+)
+vcpkg_cmake_install()
 vcpkg_copy_pdbs()
 
-if (OPENEXR_STATIC)
-  file(REMOVE_RECURSE ${CURRENT_PACKAGES_DIR}/bin ${CURRENT_PACKAGES_DIR}/debug/bin)
+vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/OpenEXR)
+vcpkg_fixup_pkgconfig()
+
+if(OPENEXR_INSTALL_TOOLS)
+    vcpkg_copy_tools(
+        TOOL_NAMES exrenvmap exrheader exrinfo exrmakepreview exrmaketiled exrmultipart exrmultiview exrstdattr exr2aces
+        AUTO_CLEAN
+    )
 endif()
 
-if (VCPKG_CMAKE_SYSTEM_NAME STREQUAL "Linux")
-  set(OPENEXR_PORT_DIR "openexr")
-else()
-  set(OPENEXR_PORT_DIR "OpenEXR")
-endif()
+file(REMOVE_RECURSE
+    "${CURRENT_PACKAGES_DIR}/debug/include"
+    "${CURRENT_PACKAGES_DIR}/debug/share"
+)
 
-file(COPY ${SOURCE_PATH}/LICENSE DESTINATION ${CURRENT_PACKAGES_DIR}/share/${OPENEXR_PORT_DIR})
-file(RENAME ${CURRENT_PACKAGES_DIR}/share/${OPENEXR_PORT_DIR}/LICENSE ${CURRENT_PACKAGES_DIR}/share/${OPENEXR_PORT_DIR}/copyright)
-
-file(COPY ${CMAKE_CURRENT_LIST_DIR}/FindOpenEXR.cmake DESTINATION ${CURRENT_PACKAGES_DIR}/share/${OPENEXR_PORT_DIR})
+file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}")
+file(INSTALL "${SOURCE_PATH}/LICENSE.md" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" RENAME copyright)
diff --git a/cmake/ports/openexr/usage b/cmake/ports/openexr/usage
new file mode 100644
index 0000000000..6b09d9db5f
--- /dev/null
+++ b/cmake/ports/openexr/usage
@@ -0,0 +1,4 @@
+openexr provides CMake targets:
+
+    find_package(OpenEXR CONFIG REQUIRED)
+    target_link_libraries(main PRIVATE OpenEXR::OpenEXR)
diff --git a/cmake/ports/openexr/vcpkg.json b/cmake/ports/openexr/vcpkg.json
new file mode 100644
index 0000000000..7f35bfac69
--- /dev/null
+++ b/cmake/ports/openexr/vcpkg.json
@@ -0,0 +1,25 @@
+{
+  "name": "openexr",
+  "version": "3.1.8",
+  "description": "OpenEXR is a high dynamic-range (HDR) image file format developed by Industrial Light & Magic for use in computer imaging applications",
+  "homepage": "https://www.openexr.com/",
+  "license": "BSD-3-Clause",
+  "supports": "!uwp",
+  "dependencies": [
+    "imath",
+    {
+      "name": "vcpkg-cmake",
+      "host": true
+    },
+    {
+      "name": "vcpkg-cmake-config",
+      "host": true
+    },
+    "zlib"
+  ],
+  "features": {
+    "tools": {
+      "description": "Build tools"
+    }
+  }
+}
diff --git a/cmake/templates/launch-c.in b/cmake/templates/launch-c.in
deleted file mode 100644
index 6c91d96dd9..0000000000
--- a/cmake/templates/launch-c.in
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/sh
-
-# Xcode generator doesn't include the compiler as the
-# first argument, Ninja and Makefiles do. Handle both cases.
-if [[ "$1" = "${CMAKE_C_COMPILER}" ]] ; then
-    shift
-fi
-
-export CCACHE_CPP2=true
-export CCACHE_HARDLINK=true
-export CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches
-exec "${C_LAUNCHER}" "${CMAKE_C_COMPILER}" "$@"
diff --git a/cmake/templates/launch-cxx.in b/cmake/templates/launch-cxx.in
deleted file mode 100644
index 4215d89c80..0000000000
--- a/cmake/templates/launch-cxx.in
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/sh
-
-# Xcode generator doesn't include the compiler as the
-# first argument, Ninja and Makefiles do. Handle both cases.
-if [[ "$1" = "${CMAKE_CXX_COMPILER}" ]] ; then
-    shift
-fi
-
-export CCACHE_CPP2=true
-export CCACHE_HARDLINK=true
-export CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches
-exec "${CXX_LAUNCHER}" "${CMAKE_CXX_COMPILER}" "$@"
diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json
index 58d5df5407..5b1c3482c1 100644
--- a/domain-server/resources/describe-settings.json
+++ b/domain-server/resources/describe-settings.json
@@ -1,5 +1,5 @@
 {
-  "version": 2.6,
+  "version": 2.7,
   "settings": [
     {
       "name": "metaverse",
@@ -402,7 +402,7 @@
             },
             {
               "label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the domain.</li><li><strong>Avatar Entities</strong><br />Sets whether a user can use avatar entities on the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li><li><strong>Replace Content</strong><br>Sets whether a user can replace entire content sets by wiping existing domain content.</li><li><strong>Get and Set Private User Data</strong><br>Sets whether a user can get and set the privateUserData entity property.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>?</a>",
-              "span": 12
+              "span": 11
             }
           ],
           "columns": [
@@ -479,6 +479,13 @@
               "type": "checkbox",
               "editable": true,
               "default": false
+            },
+            {
+              "name": "id_can_view_asset_urls",
+              "label": "View Asset URLs",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
             }
           ],
           "non-deletable-row-key": "permissions_id",
@@ -505,6 +512,7 @@
                 "id_can_rez_tmp": true,
                 "id_can_write_to_asset_server": true,
                 "id_can_get_and_set_private_user_data": true,
+                "id_can_view_asset_urls": true,
                 "permissions_id": "localhost"
             },
             {
@@ -633,6 +641,13 @@
               "type": "checkbox",
               "editable": true,
               "default": false
+            },
+            {
+              "name": "id_can_view_asset_urls",
+              "label": "View Asset URLs",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
             }
           ]
         },
@@ -752,6 +767,13 @@
               "type": "checkbox",
               "editable": true,
               "default": false
+            },
+            {
+              "name": "id_can_view_asset_urls",
+              "label": "View Asset URLs",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
             }
           ]
         },
@@ -844,6 +866,13 @@
               "type": "checkbox",
               "editable": true,
               "default": false
+            },
+            {
+              "name": "id_can_view_asset_urls",
+              "label": "View Asset URLs",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
             }
           ]
         },
@@ -936,6 +965,13 @@
               "type": "checkbox",
               "editable": true,
               "default": false
+            },
+            {
+              "name": "id_can_view_asset_urls",
+              "label": "View Asset URLs",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
             }
           ]
         },
@@ -1022,13 +1058,20 @@
               "editable": true,
               "default": false
             },
-              {
-                "name": "id_can_get_and_set_private_user_data",
-                "label": "Get and Set Private User Data",
-                "type": "checkbox",
-                "editable": true,
-                "default": false
-              }
+            {
+              "name": "id_can_get_and_set_private_user_data",
+              "label": "Get and Set Private User Data",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
+            },
+            {
+              "name": "id_can_view_asset_urls",
+              "label": "View Asset URLs",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
+            }
           ]
         },
         {
@@ -1120,6 +1163,13 @@
               "type": "checkbox",
               "editable": true,
               "default": false
+            },
+            {
+              "name": "id_can_view_asset_urls",
+              "label": "View Asset URLs",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
             }
           ]
         },
diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js
index cc7451e3f2..d3792cf36e 100644
--- a/domain-server/resources/web/settings/js/settings.js
+++ b/domain-server/resources/web/settings/js/settings.js
@@ -601,7 +601,7 @@ $(document).ready(function(){
     form += '<label class="control-label">' + label + '</label>';
     form += ' <a id="edit-network-address-port" class="domain-loading-hide" href="#">Edit</a>';
     form += '<input type="text" class="domain-loading-hide form-control" disabled></input>';
-    form += '<div class="domain-loading-hide help-block">This defines how nodes will connect to your domain. You can read more about automatic networking <a href="">here</a>.</div>';
+    form += '<div class="domain-loading-hide help-block">This defines how nodes will connect to your domain. Since the displayed setting is read back from directory server, it takes some time to update after being edited. You can read more about automatic networking <a href="">here</a>.</div>';
     form += '</div>';
 
     form = $(form);
@@ -664,7 +664,13 @@ $(document).ready(function(){
                 success: function(xhr) {
                   console.log(xhr, parseJSONResponse(xhr));
                   dialog.modal('hide');
-                  reloadDomainInfo();
+                  reloadDomainInfo(); // TODO: this one doesn't work since directory server still has old data
+                  setTimeout(function() {
+                    reloadDomainInfo();
+                  }, 16000);
+                  setTimeout(function() {
+                    reloadDomainInfo();
+                  }, 64000);
                 },
                 error:function(xhr) {
                   var data = parseJSONResponse(xhr);
diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp
index a30ddd1623..ff5ae875bc 100644
--- a/domain-server/src/DomainGatekeeper.cpp
+++ b/domain-server/src/DomainGatekeeper.cpp
@@ -353,6 +353,7 @@ void DomainGatekeeper::updateNodePermissions() {
             userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent;
             userPerms.permissions |= NodePermissions::Permission::canGetAndSetPrivateUserData;
             userPerms.permissions |= NodePermissions::Permission::canRezAvatarEntities;
+            userPerms.permissions |= NodePermissions::Permission::canViewAssetURLs;
         } else {
             // at this point we don't have a sending socket for packets from this node - assume it is the active socket
             // or the public socket if we haven't activated a socket for the node yet
@@ -446,6 +447,7 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
     userPerms.permissions |= NodePermissions::Permission::canReplaceDomainContent;
     userPerms.permissions |= NodePermissions::Permission::canGetAndSetPrivateUserData;
     userPerms.permissions |= NodePermissions::Permission::canRezAvatarEntities;
+    userPerms.permissions |= NodePermissions::Permission::canViewAssetURLs;
     newNode->setPermissions(userPerms);
     return newNode;
 }
diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp
index fb92ff526d..7d9456f059 100644
--- a/domain-server/src/DomainServer.cpp
+++ b/domain-server/src/DomainServer.cpp
@@ -73,9 +73,9 @@ Q_LOGGING_CATEGORY(domain_server_auth, "overte.domain_server.auth")
 
 const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token";
 const QString DomainServer::REPLACEMENT_FILE_EXTENSION = ".replace";
+const QString& DOMAIN_SERVER_SETTINGS_KEY = "domain_server";
 const QString PUBLIC_SOCKET_ADDRESS_KEY = "network_address";
 const QString PUBLIC_SOCKET_PORT_KEY = "network_port";
-const QString DOMAIN_UPDATE_AUTOMATIC_NETWORKING_KEY = "automatic_networking";
 const int MIN_PORT = 1;
 const int MAX_PORT = 65535;
 
@@ -1567,18 +1567,25 @@ QJsonObject jsonForDomainSocketUpdate(const SockAddr& socket) {
 }
 
 void DomainServer::performIPAddressPortUpdate(const SockAddr& newPublicSockAddr) {
-    const QString& DOMAIN_SERVER_SETTINGS_KEY = "domain_server";
     const QString& publicSocketAddress = newPublicSockAddr.getAddress().toString();
     const int publicSocketPort = newPublicSockAddr.getPort();
 
-    sendHeartbeatToMetaverse(publicSocketAddress, publicSocketPort);
+    if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE) {
+        sendHeartbeatToMetaverse(publicSocketAddress, 0);
+    } else {
+        // Full automatic networking, update both port and IP address
+        sendHeartbeatToMetaverse(publicSocketAddress, publicSocketPort);
+    }
 
     QJsonObject rootObject;
     QJsonObject domainServerObject;
     domainServerObject.insert(PUBLIC_SOCKET_ADDRESS_KEY, publicSocketAddress);
-    domainServerObject.insert(PUBLIC_SOCKET_PORT_KEY, publicSocketPort);
+    if (_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) {
+        domainServerObject.insert(PUBLIC_SOCKET_PORT_KEY, publicSocketPort);
+    }
     rootObject.insert(DOMAIN_SERVER_SETTINGS_KEY, domainServerObject);
     QJsonDocument doc(rootObject);
+    qDebug() << "DomainServer::performIPAddressPortUpdate: " << doc;
     _settingsManager.recurseJSONObjectAndOverwriteSettings(rootObject, DomainSettings);
 }
 
@@ -2487,6 +2494,16 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
                 return true;
             }
             auto domainID = domainSetting.toString();
+            qDebug() << connection->parseUrlEncodedForm();
+            auto parsed = connection->parseUrlEncodedForm();
+            if (parsed.contains(PUBLIC_SOCKET_PORT_KEY) || parsed.contains(PUBLIC_SOCKET_ADDRESS_KEY)) {
+                QJsonObject domainServerObject;
+                domainServerObject.insert(PUBLIC_SOCKET_PORT_KEY, parsed[PUBLIC_SOCKET_PORT_KEY]);
+                domainServerObject.insert(PUBLIC_SOCKET_ADDRESS_KEY, parsed[PUBLIC_SOCKET_ADDRESS_KEY]);
+                QJsonObject rootObject;
+                rootObject.insert(DOMAIN_SERVER_SETTINGS_KEY, domainServerObject);
+                _settingsManager.recurseJSONObjectAndOverwriteSettings(rootObject, DomainSettings);
+            }
             return forwardMetaverseAPIRequest(connection, url, "/api/v1/domains/" + domainID, "domain",
                                               { }, { "network_address", "network_port", "label" });
         }  else if (url.path() == URI_API_PLACES) {
diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp
index c59fc43d34..bcea7f0e01 100644
--- a/domain-server/src/DomainServerSettingsManager.cpp
+++ b/domain-server/src/DomainServerSettingsManager.cpp
@@ -547,6 +547,29 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena
 
         // No migration needed to version 2.6.
 
+        if (oldVersion < 2.7) {
+            // Default values for new canViewAssetURLs permission.
+            unpackPermissions();
+            std::list<std::unordered_map<NodePermissionsKey, NodePermissionsPointer>> permissionsSets{
+                _standardAgentPermissions.get(),
+                _agentPermissions.get(),
+                _ipPermissions.get(),
+                _macPermissions.get(),
+                _machineFingerprintPermissions.get(),
+                _groupPermissions.get(),
+                _groupForbiddens.get()
+            };
+            foreach (auto permissionsSet, permissionsSets) {
+                for (auto entry : permissionsSet) {
+                    const auto& userKey = entry.first;
+                    if (permissionsSet[userKey]->can(NodePermissions::Permission::canConnectToDomain)) {
+                        permissionsSet[userKey]->set(NodePermissions::Permission::canViewAssetURLs);
+                    }
+                }
+            }
+            packPermissions();
+        }
+
         // write the current description version to our settings
         *versionVariant = _descriptionVersion;
 
diff --git a/hifi_qt.py b/hifi_qt.py
index 254f0be1e3..ad5401aaf3 100644
--- a/hifi_qt.py
+++ b/hifi_qt.py
@@ -81,7 +81,9 @@ endif()
 
             qt_found = True
             system_qt = True
-            print("Using system Qt")
+
+            if not self.args.quiet:
+                print("Using system Qt")
 
         elif os.getenv('OVERTE_QT_PATH', "") != "":
             # 2. Using an user-provided directory.
@@ -92,7 +94,9 @@ endif()
             self.cmakePath = os.path.join(self.fullPath, 'lib', 'cmake')
 
             qt_found = True
-            print("Using Qt from " + self.fullPath)
+
+            if not self.args.quiet:
+                print("Using Qt from " + self.fullPath)
 
         else:
             # 3. Using a pre-built Qt.
@@ -135,7 +139,8 @@ endif()
             self.lockFile = os.path.join(lockDir, lockName)
 
         if qt_found:
-            print("Found pre-built Qt5")
+            if not self.args.quiet:
+                print("Found pre-built Qt5")
             return
 
         if 'Windows' == system:
@@ -147,8 +152,8 @@ endif()
             cpu_architecture = platform.machine()
 
             if 'x86_64' == cpu_architecture:
-                # `major_version()` can return blank string on rolling release distros like arch 
-                # The `or 0` conditional assignment prevents the int parsing error from hiding the useful Qt package error 
+                # `major_version()` can return blank string on rolling release distros like arch
+                # The `or 0` conditional assignment prevents the int parsing error from hiding the useful Qt package error
                 u_major = int( distro.major_version() or '0' )
                 if distro.id() == 'ubuntu' or distro.id() == 'linuxmint':
                     if (distro.id() == 'ubuntu' and u_major == 20) or distro.id() == 'linuxmint' and u_major == 20:
@@ -165,9 +170,7 @@ endif()
                 if distro.id() == 'ubuntu':
                     u_major = int( distro.major_version() )
 
-                    if u_major == 18:
-                        self.qtUrl = 'http://motofckr9k.ddns.net/vircadia_packages/qt5-install-5.15.2-ubuntu-18.04-aarch64_test.tar.xz'
-                    elif u_major == 20:
+                    if u_major == 20:
                         self.qtUrl = self.assets_url + '/dependencies/qt5/qt5-install-5.15.9-2023.05.21-kde_fb3ec282151b1ee281a24f0545a40ac6438537c2-ubuntu-20.04-aarch64.tar.xz'
                     elif u_major > 20:
                         self.__no_qt_package_error()
@@ -177,9 +180,7 @@ endif()
                 elif distro.id() == 'debian':
                     u_major = int( distro.major_version() )
 
-                    if u_major == 10:
-                        self.qtUrl = 'https://data.moto9000.moe/vircadia_packages/qt5-install-5.15.2-debian-10-aarch64.tar.xz'
-                    elif u_major > 10:
+                    if u_major > 10:
                         self.__no_qt_package_error()
                     else:
                         self.__unsupported_error()
diff --git a/hifi_vcpkg.py b/hifi_vcpkg.py
index a45975e7a5..4dc9268980 100644
--- a/hifi_vcpkg.py
+++ b/hifi_vcpkg.py
@@ -13,7 +13,7 @@ from os import path
 
 print = functools.partial(print, flush=True)
 
-# Encapsulates the vcpkg system 
+# Encapsulates the vcpkg system
 class VcpkgRepo:
     CMAKE_TEMPLATE = """
 # this file auto-generated by hifi_vcpkg.py
@@ -41,7 +41,13 @@ endif()
         else:
             self.id = hifi_utils.hashFolder(self.sourcePortsPath)[:8]
         self.configFilePath = os.path.join(args.build_root, 'vcpkg.cmake')
-        self.assets_url = self.readVar('EXTERNAL_BUILD_ASSETS')
+
+        if args.get_vcpkg_id or args.get_vcpkg_path:
+            # With these arguments no assets will be downloaded, and they may be used in conditions
+            # where the _env hack doesn't work.
+            self.assets_url = "http://no_assets.invalid"
+        else:
+            self.assets_url = self.readVar('EXTERNAL_BUILD_ASSETS')
 
         # The noClean flag indicates we're doing weird dependency maintenance stuff
         # i.e. we've got an explicit checkout of vcpkg and we don't want the script to
@@ -71,7 +77,8 @@ endif()
                 os.makedirs(self.basePath)
             self.path = os.path.join(self.basePath, self.id)
 
-        print("Using vcpkg path {}".format(self.path))
+        if not self.args.quiet:
+            print("Using vcpkg path {}".format(self.path))
         lockDir, lockName = os.path.split(self.path)
         lockName += '.lock'
         if not os.path.isdir(lockDir):
@@ -80,7 +87,7 @@ endif()
         self.lockFile = os.path.join(lockDir, lockName)
         self.tagFile = os.path.join(self.path, '.id')
         self.prebuildTagFile = os.path.join(self.path, '.prebuild')
-        # A format version attached to the tag file... increment when you want to force the build systems to rebuild 
+        # A format version attached to the tag file... increment when you want to force the build systems to rebuild
         # without the contents of the ports changing
         self.version = 1
         self.tagContents = "{}_{}".format(self.id, self.version)
@@ -108,14 +115,14 @@ endif()
         elif 'Linux' == system and 'aarch64' == machine:
             self.exe = os.path.join(self.path, 'vcpkg')
             self.bootstrapCmds = [ os.path.join(self.path, 'bootstrap-vcpkg.sh'), '-disableMetrics' ]
-            self.vcpkgUrl = self.assets_url + '/dependencies/vcpkg/vcpkg-linux_aarch64_2022.07.25.tar.xz'
-            self.vcpkgHash = '7abb7aa96200e3cb5a6d0ec1c6ee63aa7886df2d1fecf8f9ee41ebe4d2cea0d4143274222c4941cb7aca61e4048229fdfe9eb2cd36dd559dd26db871a3b3ed61'
+            self.vcpkgUrl = self.assets_url + '/dependencies/vcpkg/vcpkg-linux_aarch64_2023.11.20.tar.xz'
+            self.vcpkgHash = 'f38efba40bd4b0b6df47986e373d5535d3e787e257cf19d66ee8ee00e670a6fb95b3e824020024f3edbdcf86a0548e5bbddcc0ac7bd2ff6352a245efac8402fe'
             self.hostTriplet = 'arm64-linux'
         else:
             self.exe = os.path.join(self.path, 'vcpkg')
             self.bootstrapCmds = [ os.path.join(self.path, 'bootstrap-vcpkg.sh'), '-disableMetrics' ]
-            self.vcpkgUrl = self.assets_url + '/dependencies/vcpkg/vcpkg-linux_amd64_2022.07.25.tar.xz'
-            self.vcpkgHash = '6a1ce47ef6621e699a4627e8821ad32528c82fce62a6939d35b205da2d299aaa405b5f392df4a9e5343dd6a296516e341105fbb2dd8b48864781d129d7fba10d'
+            self.vcpkgUrl = self.assets_url + '/dependencies/vcpkg/vcpkg-linux_amd64_2023.10.19.tar.xz'
+            self.vcpkgHash = '6c26ff73d6348e121cca47e90d5358587bf83ba22852acb195b76fbf0473070b24512c8fdd3216d26f03515a79c085f239272ef87c7020cc578cc79abbbd338d'
             self.hostTriplet = 'x64-linux'
 
         if self.args.android:
@@ -188,7 +195,7 @@ endif()
         if not downloadVcpkg and not os.path.isfile(self.exe):
             print("Missing executable, boot-strapping")
             downloadVcpkg = True
-        
+
         # Make sure we have a vcpkg executable
         testFile = os.path.join(self.path, '.vcpkg-root')
         if not downloadVcpkg and not os.path.isfile(testFile):
@@ -241,7 +248,7 @@ endif()
                 hifi_utils.downloadAndExtract(self.prebuiltArchive, self.path)
                 self.writePrebuildTag()
             return
-            
+
         if qt is not None:
             self.buildEnv['QT_CMAKE_PREFIX_PATH'] = qt
 
@@ -327,12 +334,12 @@ endif()
                     write_obj.write(line)
                 else:
                     isFileChanged = True
-     
+
         if isFileChanged:
             shutil.move(newCmakeScript, cmakeScript)
         else:
             os.remove(newCmakeScript)
- 
+
 
     def writeConfig(self):
         print("Writing cmake config to {}".format(self.configFilePath))
@@ -352,7 +359,7 @@ endif()
             f.write(cmakeConfig)
 
     def cleanOldBuilds(self):
-        # FIXME because we have the base directory, and because a build will 
-        # update the tag file on every run, we can scan the base dir for sub directories containing 
+        # FIXME because we have the base directory, and because a build will
+        # update the tag file on every run, we can scan the base dir for sub directories containing
         # a tag file that is older than N days, and if found, delete the directory, recovering space
         print("Not implemented")
diff --git a/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml b/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml
index 06894d9576..5bac374fb5 100644
--- a/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml
+++ b/interface/resources/qml/hifi/dialogs/graphics/GraphicsSettings.qml
@@ -347,6 +347,10 @@ Flickable {
                             text: "Real-Time"
                             refreshRatePreset: 2 // RefreshRateProfile::REALTIME
                         }
+                        ListElement {
+                            text: "Custom"
+                            refreshRatePreset: 3 // RefreshRateProfile::CUSTOM
+                        }
                     }
 
                     HifiControlsUit.ComboBox {
@@ -362,13 +366,7 @@ Flickable {
                         currentIndex: -1
 
                         function refreshRefreshRateDropdownDisplay() {
-                            if (Performance.getRefreshRateProfile() === 0) {
-                                refreshRateDropdown.currentIndex = 0;
-                            } else if (Performance.getRefreshRateProfile() === 1) {
-                                refreshRateDropdown.currentIndex = 1;
-                            } else {
-                                refreshRateDropdown.currentIndex = 2;
-                            }
+                            refreshRateDropdown.currentIndex = Performance.getRefreshRateProfile();
                         }
 
                         Component.onCompleted: {
@@ -382,6 +380,180 @@ Flickable {
                     }
                 }
 
+                ColumnLayout {
+                    width: parent.width
+                    Layout.topMargin: 32
+                    visible: refreshRateDropdown.currentIndex == 3
+
+                    RowLayout {
+                        Layout.margins: 8
+
+                        HifiControlsUit.SpinBox {
+                            id: refreshRateCustomFocusActive
+                            decimals: 0
+                            width: 160
+                            height: 32
+                            suffix: " FPS"
+                            label: "Focus Active"
+                            realFrom: 1
+                            realTo: 1000
+                            realStepSize: 15
+                            realValue: 60
+                            colorScheme: hifi.colorSchemes.dark
+                            property var loaded: false
+
+                            Component.onCompleted: {
+                                realValue = Performance.getCustomRefreshRate(0)
+                                loaded = true
+                            }
+
+                            onRealValueChanged: {
+                                if (loaded) {
+                                    Performance.setCustomRefreshRate(0, realValue)
+                                }
+                            }
+                        }
+
+                        HifiControlsUit.SpinBox {
+                            id: refreshRateCustomFocusInactive
+                            decimals: 0
+                            width: 160
+                            height: 32
+                            suffix: " FPS"
+                            label: "Focus Inactive"
+                            realFrom: 1
+                            realTo: 1000
+                            realStepSize: 15
+                            realValue: 60
+                            colorScheme: hifi.colorSchemes.dark
+                            property var loaded: false
+
+                            Component.onCompleted: {
+                                realValue = Performance.getCustomRefreshRate(1)
+                                loaded = true
+                            }
+
+                            onRealValueChanged: {
+                                if (loaded) {
+                                    Performance.setCustomRefreshRate(1, realValue)
+                                }
+                            }
+                        }
+                    }
+
+                    RowLayout {
+                        Layout.margins: 8
+
+                        HifiControlsUit.SpinBox {
+                            id: refreshRateCustomUnfocus
+                            decimals: 0
+                            width: 160
+                            height: 32
+                            suffix: " FPS"
+                            label: "Unfocus"
+                            realFrom: 1
+                            realTo: 1000
+                            realStepSize: 15
+                            realValue: 60
+                            colorScheme: hifi.colorSchemes.dark
+                            property var loaded: false
+
+                            Component.onCompleted: {
+                                realValue = Performance.getCustomRefreshRate(2)
+                                loaded = true
+                            }
+
+                            onRealValueChanged: {
+                                if (loaded) {
+                                    Performance.setCustomRefreshRate(2, realValue);
+                                }
+                            }
+                        }
+
+                        HifiControlsUit.SpinBox {
+                            id: refreshRateCustomMinimized
+                            decimals: 0
+                            width: 160
+                            height: 32
+                            suffix: " FPS"
+                            label: "Minimized"
+                            realFrom: 1
+                            realTo: 1000
+                            realStepSize: 1
+                            realValue: 60
+                            colorScheme: hifi.colorSchemes.dark
+                            property var loaded: false
+
+                            Component.onCompleted: {
+                                realValue = Performance.getCustomRefreshRate(3)
+                                loaded = true
+                            }
+
+                            onRealValueChanged: {
+                                if (loaded) {
+                                    Performance.setCustomRefreshRate(3, realValue)
+                                }
+                            }
+                        }
+                    }
+
+                    RowLayout {
+                        Layout.margins: 8
+
+                        HifiControlsUit.SpinBox {
+                            id: refreshRateCustomStartup
+                            decimals: 0
+                            width: 160
+                            height: 32
+                            suffix: " FPS"
+                            label: "Startup"
+                            realFrom: 1
+                            realTo: 1000
+                            realStepSize: 15
+                            realValue: 60
+                            colorScheme: hifi.colorSchemes.dark
+                            property var loaded: false
+
+                            Component.onCompleted: {
+                                realValue = Performance.getCustomRefreshRate(4)
+                                loaded = true
+                            }
+
+                            onRealValueChanged: {
+                                if (loaded) {
+                                    Performance.setCustomRefreshRate(4, realValue)
+                                }
+                            }
+                        }
+
+                        HifiControlsUit.SpinBox {
+                            id: refreshRateCustomShutdown
+                            decimals: 0
+                            width: 160
+                            height: 32
+                            suffix: " FPS"
+                            label: "Shutdown"
+                            realFrom: 1
+                            realTo: 1000
+                            realStepSize: 15
+                            realValue: 60
+                            colorScheme: hifi.colorSchemes.dark
+                            property var loaded: false
+
+                            Component.onCompleted: {
+                                realValue = Performance.getCustomRefreshRate(5)
+                                loaded = true
+                            }
+
+                            onRealValueChanged: {
+                                if (loaded) {
+                                    Performance.setCustomRefreshRate(5, realValue)
+                                }
+                            }
+                        }
+                    }
+                }
+
                 Item {
                     Layout.preferredWidth: parent.width
                     Layout.preferredHeight: 35
diff --git a/interface/resources/qml/hifi/dialogs/security/ScriptSecurity.qml b/interface/resources/qml/hifi/dialogs/security/ScriptSecurity.qml
new file mode 100644
index 0000000000..de7304b6fb
--- /dev/null
+++ b/interface/resources/qml/hifi/dialogs/security/ScriptSecurity.qml
@@ -0,0 +1,152 @@
+//
+//  ScriptPermissions.cpp
+//  libraries/script-engine/src/ScriptPermissions.cpp
+//
+//  Created by dr Karol Suprynowicz on 2024/03/24.
+//  Copyright 2024 Overte e.V.
+//
+//  Based on EntityScriptQMLWhitelist.qml
+//  Created by Kalila L. on 2019.12.05 | realities.dev | somnilibertas@gmail.com
+//  Copyright 2019 Kalila L.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+// Security settings for the script engines
+
+import Hifi 1.0 as Hifi
+import QtQuick 2.8
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.12
+import stylesUit 1.0 as HifiStylesUit
+import controlsUit 1.0 as HiFiControls
+import PerformanceEnums 1.0
+import "../../../windows"
+
+
+Rectangle {
+    id: parentBody;
+
+    function getWhitelistAsText() {
+        var whitelist = Settings.getValue("private/scriptPermissionGetAvatarURLSafeURLs");
+        var arrayWhitelist = whitelist.replace(",", "\n");
+        return arrayWhitelist;
+    }
+
+    function setWhitelistAsText(whitelistText) {
+        Settings.setValue("private/scriptPermissionGetAvatarURLSafeURLs", whitelistText.text);
+        notificationText.text = "Whitelist saved.";
+    }
+
+    function setAvatarProtection(enabled) {
+        Settings.setValue("private/scriptPermissionGetAvatarURLEnable", enabled);
+        console.info("Setting Protect Avatar URLs to:", enabled);
+    }
+
+    anchors.fill: parent
+    width: parent.width;
+    height: 120;
+    color: "#80010203";
+
+    HifiStylesUit.RalewayRegular {
+        id: titleText;
+        text: "Protect Avatar URLs"
+        // Text size
+        size: 24;
+        // Style
+        color: "white";
+        elide: Text.ElideRight;
+        // Anchors
+        anchors.top: parent.top;
+        anchors.left: parent.left;
+        anchors.leftMargin: 20;
+        anchors.right: parent.right;
+        anchors.rightMargin: 20;
+        height: 60;
+
+        CheckBox {
+            id: whitelistEnabled;
+
+            checked: Settings.getValue("private/scriptPermissionGetAvatarURLEnable", true);
+
+            anchors.right: parent.right;
+            anchors.top: parent.top;
+            anchors.topMargin: 10;
+            onToggled: {
+                setAvatarProtection(whitelistEnabled.checked)
+            }
+
+            Label {
+                text: "Enabled"
+                color: "white"
+                font.pixelSize: 18;
+                anchors.right: parent.left;
+                anchors.top: parent.top;
+                anchors.topMargin: 10;
+            }
+        }
+    }
+
+    Rectangle {
+        id: textAreaRectangle;
+        color: "black";
+        width: parent.width;
+        height: 250;
+        anchors.top: titleText.bottom;
+    
+        ScrollView {
+            id: textAreaScrollView
+            anchors.fill: parent;
+            width: parent.width
+            height: parent.height
+            contentWidth: parent.width
+            contentHeight: parent.height
+            clip: false;
+
+            TextArea {
+                id: whitelistTextArea
+                text: getWhitelistAsText();
+                onTextChanged: notificationText.text = "";
+                width: parent.width;
+                height: parent.height;
+                font.family: "Ubuntu";
+                font.pointSize: 12;
+                color: "white";
+            }
+        }
+        
+        Button {
+            id: saveChanges
+            anchors.topMargin: 5;
+            anchors.leftMargin: 20;
+            anchors.rightMargin: 20;
+            x: textAreaRectangle.x + textAreaRectangle.width - width - 15;
+            y: textAreaRectangle.y + textAreaRectangle.height - height;
+            contentItem: Text {
+                text: saveChanges.text
+                font.family: "Ubuntu";
+                font.pointSize: 12;
+                opacity: enabled ? 1.0 : 0.3
+                color: "black"
+                horizontalAlignment: Text.AlignHCenter
+                verticalAlignment: Text.AlignVCenter
+                elide: Text.ElideRight
+            }
+            text: "Save Changes"
+            onClicked: setWhitelistAsText(whitelistTextArea)
+          
+            HifiStylesUit.RalewayRegular {
+                id: notificationText;
+                text: ""
+                // Text size
+                size: 16;
+                // Style
+                color: "white";
+                elide: Text.ElideLeft;
+                // Anchors
+                anchors.right: parent.left;
+                anchors.rightMargin: 10;
+            }
+        }
+    }
+}
diff --git a/interface/resources/qml/hifi/simplifiedUI/helpApp/faq/HelpFAQ.qml b/interface/resources/qml/hifi/simplifiedUI/helpApp/faq/HelpFAQ.qml
index 7bfb711e29..6272598e29 100644
--- a/interface/resources/qml/hifi/simplifiedUI/helpApp/faq/HelpFAQ.qml
+++ b/interface/resources/qml/hifi/simplifiedUI/helpApp/faq/HelpFAQ.qml
@@ -3,6 +3,7 @@
 //
 //  Created by Zach Fox on 2019-08-08
 //  Copyright 2019 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -78,7 +79,7 @@ Item {
             temporaryText: "Viewing!"
 
             onClicked: {
-                Qt.openUrlExternally("https://www.highfidelity.com/knowledge");
+                Qt.openUrlExternally("https://overte.org/");
             }
         }
 
diff --git a/interface/resources/qml/hifi/simplifiedUI/helpApp/support/HelpSupport.qml b/interface/resources/qml/hifi/simplifiedUI/helpApp/support/HelpSupport.qml
index 156e5cf5fd..8d294be95d 100644
--- a/interface/resources/qml/hifi/simplifiedUI/helpApp/support/HelpSupport.qml
+++ b/interface/resources/qml/hifi/simplifiedUI/helpApp/support/HelpSupport.qml
@@ -3,6 +3,7 @@
 //
 //  Created by Zach Fox on 2019-08-20
 //  Copyright 2019 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -76,7 +77,7 @@ Item {
             temporaryText: "Opening browser..."
 
             onClicked: {
-                Qt.openUrlExternally("https://www.highfidelity.com/knowledge/kb-tickets/new");
+                Qt.openUrlExternally("https://overte.org/contact.html");
             }
         }
     }
diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/dev/Dev.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/dev/Dev.qml
index 359b1bb670..68a66c11c6 100644
--- a/interface/resources/qml/hifi/simplifiedUI/settingsApp/dev/Dev.qml
+++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/dev/Dev.qml
@@ -3,6 +3,7 @@
 //
 //  Created by Zach Fox on 2019-06-11
 //  Copyright 2019 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -93,9 +94,9 @@ Flickable {
                     width: parent.width
                     height: 18
                     labelTextOn: "Keep Old Menus (File, Edit, etc)"
-                    checked: Settings.getValue("simplifiedUI/keepMenus", false);
+                    checked: Settings.getValue("simplifiedUI/keepMenus", true);
                     onClicked: {
-                        Settings.setValue("simplifiedUI/keepMenus", !Settings.getValue("simplifiedUI/keepMenus", false));
+                        Settings.setValue("simplifiedUI/keepMenus", !Settings.getValue("simplifiedUI/keepMenus", true));
                     }
                 }
 
diff --git a/interface/resources/qml/hifi/simplifiedUI/settingsApp/general/General.qml b/interface/resources/qml/hifi/simplifiedUI/settingsApp/general/General.qml
index a61fd68239..db65d8868a 100644
--- a/interface/resources/qml/hifi/simplifiedUI/settingsApp/general/General.qml
+++ b/interface/resources/qml/hifi/simplifiedUI/settingsApp/general/General.qml
@@ -3,6 +3,7 @@
 //
 //  Created by Zach Fox on 2019-05-06
 //  Copyright 2019 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -175,7 +176,7 @@ Flickable {
                 spacing: simplifiedUI.margins.settings.spacingBetweenRadiobuttons
 
                 SimplifiedControls.RadioButton {
-                    id: performanceLow
+                    id: performanceLowPower
                     text: "Low Power Quality" + (PlatformInfo.getTierProfiled() === PerformanceEnums.LOW_POWER ? " (Recommended)" : "")
                     checked: Performance.getPerformancePreset() === PerformanceEnums.LOW_POWER
                     onClicked: {
diff --git a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Button.qml b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Button.qml
index e4a1c2af08..fe2a6b83eb 100644
--- a/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Button.qml
+++ b/interface/resources/qml/hifi/simplifiedUI/simplifiedControls/Button.qml
@@ -3,6 +3,7 @@
 //
 //  Created by Zach Fox on 2019-05-08
 //  Copyright 2019 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -96,12 +97,11 @@ Original.Button {
         }
     }
 
-    contentItem:  HifiStylesUit.FiraSansMedium {
+    contentItem:  Text {
         id: buttonText
         //topPadding: -2 // Necessary for proper alignment using Graphik Medium
         wrapMode: Text.Wrap
         color: enabled ? simplifiedUI.colors.controls.button.text.enabled : simplifiedUI.colors.controls.button.text.disabled
-        size: simplifiedUI.sizes.controls.button.textSize
         verticalAlignment: Text.AlignVCenter
         horizontalAlignment: Text.AlignHCenter
         text: root.text
diff --git a/interface/resources/qml/hifi/tablet/ControllerSettings.qml b/interface/resources/qml/hifi/tablet/ControllerSettings.qml
index 492ec265d4..0115961d02 100644
--- a/interface/resources/qml/hifi/tablet/ControllerSettings.qml
+++ b/interface/resources/qml/hifi/tablet/ControllerSettings.qml
@@ -27,7 +27,7 @@ Item {
     width: parent.width
 
     property string title: "Controls"
-    property var openVRDevices: ["HTC Vive", "Valve Index", "Valve HMD", "Valve", "WindowsMR"]
+    property var openVRDevices: ["HTC Vive", "Valve Index", "Valve HMD", "Valve", "WindowsMR", "Oculus"]
 
     HifiConstants { id: hifi }
 
diff --git a/interface/resources/serverless/Scripts/activator-doppleganger.js b/interface/resources/serverless/Scripts/activator-doppleganger.js
index 89661683d3..3e39ea3f62 100644
--- a/interface/resources/serverless/Scripts/activator-doppleganger.js
+++ b/interface/resources/serverless/Scripts/activator-doppleganger.js
@@ -64,7 +64,7 @@
 
     function startDopplegangerShow(entityID) {
         var properties = Entities.getEntityProperties(entityID, ["position", "rotation"]);
-        var avatarPosition = MyAvatar.position;
+        var avatarPosition = MyAvatar.feetPosition;
         var drawPosition = {
             "x": properties.position.x,
             "y": avatarPosition.y,
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 931c41703b..e756ca276a 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -724,8 +724,8 @@ extern DisplayPluginList getDisplayPlugins();
 extern InputPluginList getInputPlugins();
 extern void saveInputPluginSettings(const InputPluginList& plugins);
 
-bool setupEssentials(int& argc, char** argv, const QCommandLineParser& parser, bool runningMarkerExisted) {
-    qInstallMessageHandler(messageHandler);
+bool setupEssentials(const QCommandLineParser& parser, bool runningMarkerExisted) {
+
 
 
     const int listenPort = parser.isSet("listenPort") ? parser.value("listenPort").toInt() : INVALID_PORT;
@@ -743,6 +743,7 @@ bool setupEssentials(int& argc, char** argv, const QCommandLineParser& parser, b
 
     bool previousSessionCrashed { false };
     if (!inTestMode) {
+        // TODO: FIX
         previousSessionCrashed = CrashRecoveryHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt);
     }
 
@@ -763,13 +764,12 @@ bool setupEssentials(int& argc, char** argv, const QCommandLineParser& parser, b
         }
     }
 
-    // Tell the plugin manager about our statically linked plugins
+
+
     DependencyManager::set<ScriptInitializers>();
-    DependencyManager::set<PluginManager>();
+
+    // Tell the plugin manager about our statically linked plugins
     auto pluginManager = PluginManager::getInstance();
-    pluginManager->setInputPluginProvider([] { return getInputPlugins(); });
-    pluginManager->setDisplayPluginProvider([] { return getDisplayPlugins(); });
-    pluginManager->setInputPluginSettingsPersister([](const InputPluginList& plugins) { saveInputPluginSettings(plugins); });
     if (auto steamClient = pluginManager->getSteamClientPlugin()) {
         steamClient->init();
     }
@@ -777,6 +777,7 @@ bool setupEssentials(int& argc, char** argv, const QCommandLineParser& parser, b
         oculusPlatform->init();
     }
 
+
     PROFILE_SET_THREAD_NAME("Main Thread");
 
 #if defined(Q_OS_WIN)
@@ -901,7 +902,12 @@ bool setupEssentials(int& argc, char** argv, const QCommandLineParser& parser, b
     DependencyManager::set<CompositorHelper>();
     DependencyManager::set<OffscreenQmlSurfaceCache>();
     DependencyManager::set<EntityScriptClient>();
+
     DependencyManager::set<EntityScriptServerLogClient>();
+    auto scriptEngines = DependencyManager::get<ScriptEngines>();
+    auto entityScriptServerLog = DependencyManager::get<EntityScriptServerLogClient>();
+    QObject::connect(scriptEngines.data(), &ScriptEngines::requestingEntityScriptServerLog, entityScriptServerLog.data(), &EntityScriptServerLogClient::requestMessagesForScriptEngines);
+
     DependencyManager::set<GooglePolyScriptingInterface>();
     DependencyManager::set<OctreeStatsProvider>(nullptr, qApp->getOcteeSceneStats());
     DependencyManager::set<AvatarBookmarks>();
@@ -993,8 +999,7 @@ bool Application::initMenu() {
 Application::Application(
     int& argc, char** argv,
     const QCommandLineParser& parser,
-    QElapsedTimer& startupTimer,
-    bool runningMarkerExisted
+    QElapsedTimer& startupTimer
 ) :
     QApplication(argc, argv),
     _window(new MainWindow(desktop())),
@@ -1004,10 +1009,7 @@ Application::Application(
 #ifndef Q_OS_ANDROID
     _logger(new FileLogger(this)),
 #endif
-    _previousSessionCrashed(setupEssentials(argc, argv, parser, runningMarkerExisted)),
-    _entitySimulation(std::make_shared<PhysicalEntitySimulation>()),
-    _physicsEngine(std::make_shared<PhysicsEngine>(Vectors::ZERO)),
-    _entityClipboard(std::make_shared<EntityTree>()),
+    _previousSessionCrashed(false), //setupEssentials(parser, false)),
     _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION),
     _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES),
     _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT),
@@ -1032,12 +1034,72 @@ Application::Application(
     _snapshotSound(nullptr),
     _sampleSound(nullptr)
 {
-    auto steamClient = PluginManager::getInstance()->getSteamClientPlugin();
-    setProperty(hifi::properties::STEAM, (steamClient && steamClient->isRunning()));
     setProperty(hifi::properties::CRASHED, _previousSessionCrashed);
 
     LogHandler::getInstance().moveToThread(thread());
     LogHandler::getInstance().setupRepeatedMessageFlusher();
+    qInstallMessageHandler(messageHandler);
+
+    DependencyManager::set<PathUtils>();
+}
+
+void Application::initializePluginManager(const QCommandLineParser& parser) {
+    DependencyManager::set<PluginManager>();
+    auto pluginManager = PluginManager::getInstance();
+
+    // To avoid any confusion: the getInputPlugins and getDisplayPlugins are not the ones
+    // from PluginManager, but functions exported by input-plugins/InputPlugin.cpp and
+    // display-plugins/DisplayPlugin.cpp.
+    //
+    // These functions provide the plugin manager with static default plugins.
+    pluginManager->setInputPluginProvider([] { return getInputPlugins(); });
+    pluginManager->setDisplayPluginProvider([] { return getDisplayPlugins(); });
+    pluginManager->setInputPluginSettingsPersister([](const InputPluginList& plugins) { saveInputPluginSettings(plugins); });
+
+
+    // This must be a member function -- PluginManager must exist, and for that
+    // QApplication must exist, or it can't find the plugin path, as QCoreApplication:applicationDirPath
+    // won't work yet.
+
+    if (parser.isSet("display")) {
+        auto preferredDisplays = parser.value("display").split(',', Qt::SkipEmptyParts);
+        qInfo() << "Setting prefered display plugins:" << preferredDisplays;
+        PluginManager::getInstance()->setPreferredDisplayPlugins(preferredDisplays);
+    }
+
+    if (parser.isSet("disableDisplayPlugins")) {
+        auto disabledDisplays = parser.value("disableDisplayPlugins").split(',', Qt::SkipEmptyParts);
+        qInfo() << "Disabling following display plugins:"  << disabledDisplays;
+        PluginManager::getInstance()->disableDisplays(disabledDisplays);
+    }
+
+    if (parser.isSet("disableInputPlugins")) {
+        auto disabledInputs = parser.value("disableInputPlugins").split(',', Qt::SkipEmptyParts);
+        qInfo() << "Disabling following input plugins:" << disabledInputs;
+        PluginManager::getInstance()->disableInputs(disabledInputs);
+    }
+
+}
+
+void Application::initialize(const QCommandLineParser &parser) {
+
+    //qCDebug(interfaceapp) << "Setting up essentials";
+    setupEssentials(parser, _previousSessionCrashed);
+    qCDebug(interfaceapp) << "Initializing application";
+
+    _entitySimulation = std::make_shared<PhysicalEntitySimulation>();
+    _physicsEngine = std::make_shared<PhysicsEngine>(Vectors::ZERO);
+    _entityClipboard = std::make_shared<EntityTree>();
+    _octreeProcessor = std::make_shared<OctreePacketProcessor>();
+    _entityEditSender = std::make_shared<EntityEditPacketSender>();
+    _graphicsEngine = std::make_shared<GraphicsEngine>();
+    _applicationOverlay = std::make_shared<ApplicationOverlay>();
+
+
+
+    auto steamClient = PluginManager::getInstance()->getSteamClientPlugin();
+    setProperty(hifi::properties::STEAM, (steamClient && steamClient->isRunning()));
+
 
     {
         if (parser.isSet("testScript")) {
@@ -1405,7 +1467,7 @@ Application::Application(
     connect(myAvatar.get(), &MyAvatar::positionGoneTo, this, [this] {
         if (!_physicsEnabled) {
             // when we arrive somewhere without physics enabled --> startSafeLanding
-            _octreeProcessor.startSafeLanding();
+            _octreeProcessor->startSafeLanding();
         }
     }, Qt::QueuedConnection);
 
@@ -1578,9 +1640,9 @@ Application::Application(
     qCDebug(interfaceapp, "init() complete.");
 
     // create thread for parsing of octree data independent of the main network and rendering threads
-    _octreeProcessor.initialize(_enableProcessOctreeThread);
-    connect(&_octreeProcessor, &OctreePacketProcessor::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch);
-    _entityEditSender.initialize(_enableProcessOctreeThread);
+    _octreeProcessor->initialize(_enableProcessOctreeThread);
+    connect(_octreeProcessor.get(), &OctreePacketProcessor::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch);
+    _entityEditSender->initialize(_enableProcessOctreeThread);
 
     _idleLoopStdev.reset();
 
@@ -1698,7 +1760,7 @@ Application::Application(
         userActivityLogger.logAction("launch", properties);
     }
 
-    _entityEditSender.setMyAvatar(myAvatar.get());
+    _entityEditSender->setMyAvatar(myAvatar.get());
 
     // The entity octree will have to know about MyAvatar for the parentJointName import
     getEntities()->getTree()->setMyAvatar(myAvatar);
@@ -1707,7 +1769,7 @@ Application::Application(
     // For now we're going to set the PPS for outbound packets to be super high, this is
     // probably not the right long term solution. But for now, we're going to do this to
     // allow you to move an entity around in your hand
-    _entityEditSender.setPacketsPerSecond(3000); // super high!!
+    _entityEditSender->setPacketsPerSecond(3000); // super high!!
 
     // Make sure we don't time out during slow operations at startup
     updateHeartbeat();
@@ -2375,7 +2437,7 @@ Application::Application(
 
     connect(this, &Application::applicationStateChanged, this, &Application::activeChanged);
     connect(_window, SIGNAL(windowMinimizedChanged(bool)), this, SLOT(windowMinimizedChanged(bool)));
-    qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0);
+    qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)_sessionRunTimer.elapsed() / 1000.0);
 
     EntityTreeRenderer::setEntitiesShouldFadeFunction([this]() {
         SharedNodePointer entityServerNode = DependencyManager::get<NodeList>()->soloNodeOfType(NodeType::EntityServer);
@@ -2573,7 +2635,7 @@ Application::Application(
     }
 
     _pendingIdleEvent = false;
-    _graphicsEngine.startup();
+    _graphicsEngine->startup();
 
     qCDebug(interfaceapp) << "Directory Service session ID is" << uuidStringWithoutCurlyBraces(accountManager->getSessionID());
 
@@ -2880,43 +2942,59 @@ void Application::cleanupBeforeQuit() {
 
 Application::~Application() {
     // remove avatars from physics engine
-    auto avatarManager = DependencyManager::get<AvatarManager>();
-    avatarManager->clearOtherAvatars();
-    auto myCharacterController = getMyAvatar()->getCharacterController();
-    myCharacterController->clearDetailedMotionStates();
+    if (auto avatarManager = DependencyManager::get<AvatarManager>()) {
+        // AvatarManager may not yet exist in case of an early exit
 
-    PhysicsEngine::Transaction transaction;
-    avatarManager->buildPhysicsTransaction(transaction);
-    _physicsEngine->processTransaction(transaction);
-    avatarManager->handleProcessedPhysicsTransaction(transaction);
-    avatarManager->deleteAllAvatars();
+        avatarManager->clearOtherAvatars();
+        auto myCharacterController = getMyAvatar()->getCharacterController();
+        myCharacterController->clearDetailedMotionStates();
 
-    _physicsEngine->setCharacterController(nullptr);
+        PhysicsEngine::Transaction transaction;
+        avatarManager->buildPhysicsTransaction(transaction);
+        _physicsEngine->processTransaction(transaction);
+        avatarManager->handleProcessedPhysicsTransaction(transaction);
+        avatarManager->deleteAllAvatars();
+    }
+
+    if (_physicsEngine) {
+        _physicsEngine->setCharacterController(nullptr);
+    }
 
     // the _shapeManager should have zero references
     _shapeManager.collectGarbage();
     assert(_shapeManager.getNumShapes() == 0);
 
-    // shutdown graphics engine
-    _graphicsEngine.shutdown();
+    if (_graphicsEngine) {
+        // shutdown graphics engine
+        _graphicsEngine->shutdown();
+    }
 
     _gameWorkload.shutdown();
 
     DependencyManager::destroy<Preferences>();
     PlatformHelper::shutdown();
 
-    _entityClipboard->eraseAllOctreeElements();
-    _entityClipboard.reset();
-
-    _octreeProcessor.terminate();
-    _entityEditSender.terminate();
-
-    if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) {
-        steamClient->shutdown();
+    if (_entityClipboard) {
+        _entityClipboard->eraseAllOctreeElements();
+        _entityClipboard.reset();
     }
 
-    if (auto oculusPlatform = PluginManager::getInstance()->getOculusPlatformPlugin()) {
-        oculusPlatform->shutdown();
+    if (_octreeProcessor) {
+        _octreeProcessor->terminate();
+    }
+
+    if (_entityEditSender) {
+        _entityEditSender->terminate();
+    }
+
+    if (auto pluginManager = PluginManager::getInstance()) {
+        if (auto steamClient = pluginManager->getSteamClientPlugin()) {
+            steamClient->shutdown();
+        }
+
+        if (auto oculusPlatform = pluginManager->getOculusPlatformPlugin()) {
+            oculusPlatform->shutdown();
+        }
     }
 
     DependencyManager::destroy<PluginManager>();
@@ -2944,7 +3022,9 @@ Application::~Application() {
     DependencyManager::destroy<GeometryCache>();
     DependencyManager::destroy<ScreenshareScriptingInterface>();
 
-    DependencyManager::get<ResourceManager>()->cleanup();
+    if (auto resourceManager = DependencyManager::get<ResourceManager>()) {
+        resourceManager->cleanup();
+    }
 
     // remove the NodeList from the DependencyManager
     DependencyManager::destroy<NodeList>();
@@ -2958,13 +3038,14 @@ Application::~Application() {
     _window->deleteLater();
 
     // make sure that the quit event has finished sending before we take the application down
-    auto closeEventSender = DependencyManager::get<CloseEventSender>();
-    while (!closeEventSender->hasFinishedQuitEvent() && !closeEventSender->hasTimedOutQuitEvent()) {
-        // sleep a little so we're not spinning at 100%
-        std::this_thread::sleep_for(std::chrono::milliseconds(10));
+    if (auto closeEventSender = DependencyManager::get<CloseEventSender>()) {
+        while (!closeEventSender->hasFinishedQuitEvent() && !closeEventSender->hasTimedOutQuitEvent()) {
+            // sleep a little so we're not spinning at 100%
+            std::this_thread::sleep_for(std::chrono::milliseconds(10));
+        }
+        // quit the thread used by the closure event sender
+        closeEventSender->thread()->quit();
     }
-    // quit the thread used by the closure event sender
-    closeEventSender->thread()->quit();
 
     // Can't log to file past this point, FileLogger about to be deleted
     qInstallMessageHandler(LogHandler::verboseMessageHandler);
@@ -3104,7 +3185,7 @@ void Application::initializeGL() {
     glClear(GL_COLOR_BUFFER_BIT);
     _glWidget->swapBuffers();
 
-    _graphicsEngine.initializeGPU(_glWidget);
+    _graphicsEngine->initializeGPU(_glWidget);
 }
 
 void Application::initializeDisplayPlugins() {
@@ -3116,7 +3197,7 @@ void Application::initializeDisplayPlugins() {
     // Once time initialization code
     DisplayPluginPointer targetDisplayPlugin;
     for(const auto& displayPlugin : displayPlugins) {
-        displayPlugin->setContext(_graphicsEngine.getGPUContext());
+        displayPlugin->setContext(_graphicsEngine->getGPUContext());
         if (displayPlugin->getName() == lastActiveDisplayPluginName) {
             targetDisplayPlugin = displayPlugin;
         }
@@ -3168,7 +3249,7 @@ void Application::initializeDisplayPlugins() {
 void Application::initializeRenderEngine() {
     // FIXME: on low end systems os the shaders take up to 1 minute to compile, so we pause the deadlock watchdog thread.
     DeadlockWatchdogThread::withPause([&] {
-        _graphicsEngine.initializeRender();
+        _graphicsEngine->initializeRender();
         DependencyManager::get<Keyboard>()->registerKeyboardHighlighting();
     });
 }
@@ -3225,7 +3306,7 @@ void Application::initializeUi() {
 
             // END PULL SAFEURLS FROM INTERFACE.JSON Settings
 
-            if (AUTHORIZED_EXTERNAL_QML_SOURCE.isParentOf(url)) {
+            if (QUrl(NetworkingConstants::OVERTE_COMMUNITY_APPLICATIONS).isParentOf(url)) {
                 return true;
             } else {
                 for (const auto& str : safeURLS) {
@@ -3425,7 +3506,7 @@ void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) {
     surfaceContext->setContextProperty("Recording", DependencyManager::get<RecordingScriptingInterface>().data());
     surfaceContext->setContextProperty("Preferences", DependencyManager::get<Preferences>().data());
     surfaceContext->setContextProperty("AddressManager", DependencyManager::get<AddressManager>().data());
-    surfaceContext->setContextProperty("FrameTimings", &_graphicsEngine._frameTimingsScriptingInterface);
+    surfaceContext->setContextProperty("FrameTimings", &_graphicsEngine->_frameTimingsScriptingInterface);
     surfaceContext->setContextProperty("Rates", new RatesScriptingInterface(this));
 
     surfaceContext->setContextProperty("TREE_SCALE", TREE_SCALE);
@@ -4061,7 +4142,7 @@ std::map<QString, QString> Application::prepareServerlessDomainContents(QUrl dom
     bool success = tmpTree->readFromByteArray(domainURL.toString(), data);
     if (success) {
         tmpTree->reaverageOctreeElements();
-        tmpTree->sendEntities(&_entityEditSender, getEntities()->getTree(), "domain", 0, 0, 0);
+        tmpTree->sendEntities(_entityEditSender.get(), getEntities()->getTree(), "domain", 0, 0, 0);
     }
     std::map<QString, QString> namedPaths = tmpTree->getNamedPaths();
 
@@ -4131,8 +4212,8 @@ void Application::onPresent(quint32 frameCount) {
         postEvent(this, new QEvent((QEvent::Type)ApplicationEvent::Idle), Qt::HighEventPriority);
     }
     expected = false;
-    if (_graphicsEngine.checkPendingRenderEvent() && !isAboutToQuit()) {
-        postEvent(_graphicsEngine._renderEventHandler, new QEvent((QEvent::Type)ApplicationEvent::Render));
+    if (_graphicsEngine->checkPendingRenderEvent() && !isAboutToQuit()) {
+        postEvent(_graphicsEngine->_renderEventHandler, new QEvent((QEvent::Type)ApplicationEvent::Render));
     }
 }
 
@@ -4202,7 +4283,9 @@ bool Application::handleFileOpenEvent(QFileOpenEvent* fileEvent) {
 }
 
 bool Application::notify(QObject * object, QEvent * event) {
-    if (thread() == QThread::currentThread()) {
+    if (thread() == QThread::currentThread() && _profilingInitialized ) {
+        // _profilingInitialized gets set once we're reading for profiling.
+        // this prevents a deadlock due to profiling not working yet
         PROFILE_RANGE_IF_LONGER(app, "notify", 2)
         return QApplication::notify(object, event);
     }
@@ -5247,8 +5330,8 @@ void Application::idle() {
     PROFILE_COUNTER_IF_CHANGED(app, "pendingDownloads", uint32_t, ResourceCache::getPendingRequestCount());
     PROFILE_COUNTER_IF_CHANGED(app, "currentProcessing", int, DependencyManager::get<StatTracker>()->getStat("Processing").toInt());
     PROFILE_COUNTER_IF_CHANGED(app, "pendingProcessing", int, DependencyManager::get<StatTracker>()->getStat("PendingProcessing").toInt());
-    auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration();
-    PROFILE_COUNTER_IF_CHANGED(render, "gpuTime", float, (float)_graphicsEngine.getGPUContext()->getFrameTimerGPUAverage());
+    auto renderConfig = _graphicsEngine->getRenderEngine()->getConfiguration();
+    PROFILE_COUNTER_IF_CHANGED(render, "gpuTime", float, (float)_graphicsEngine->getGPUContext()->getFrameTimerGPUAverage());
 
     PROFILE_RANGE(app, __FUNCTION__);
 
@@ -5615,7 +5698,7 @@ bool Application::importEntities(const QString& urlOrFilename, const bool isObse
 }
 
 QVector<EntityItemID> Application::pasteEntities(const QString& entityHostType, float x, float y, float z) {
-    return _entityClipboard->sendEntities(&_entityEditSender, getEntities()->getTree(), entityHostType, x, y, z);
+    return _entityClipboard->sendEntities(_entityEditSender.get(), getEntities()->getTree(), entityHostType, x, y, z);
 }
 
 void Application::init() {
@@ -5665,7 +5748,7 @@ void Application::init() {
     _physicsEngine->init();
 
     EntityTreePointer tree = getEntities()->getTree();
-    _entitySimulation->init(tree, _physicsEngine, &_entityEditSender);
+    _entitySimulation->init(tree, _physicsEngine, _entityEditSender.get());
     tree->setSimulation(_entitySimulation);
 
     auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
@@ -5689,7 +5772,7 @@ void Application::init() {
         }
     }, Qt::QueuedConnection);
 
-    _gameWorkload.startup(getEntities()->getWorkloadSpace(), _graphicsEngine.getRenderScene(), _entitySimulation);
+    _gameWorkload.startup(getEntities()->getWorkloadSpace(), _graphicsEngine->getRenderScene(), _entitySimulation);
     _entitySimulation->setWorkloadSpace(getEntities()->getWorkloadSpace());
 }
 
@@ -5861,7 +5944,7 @@ void Application::updateLOD(float deltaTime) const {
     // adjust it unless we were asked to disable this feature, or if we're currently in throttleRendering mode
     if (!isThrottleRendering()) {
         float presentTime = getActiveDisplayPlugin()->getAveragePresentTime();
-        float engineRunTime = (float)(_graphicsEngine.getRenderEngine()->getConfiguration().get()->getCPURunTime());
+        float engineRunTime = (float)(_graphicsEngine->getRenderEngine()->getConfiguration().get()->getCPURunTime());
         float gpuTime = getGPUContext()->getFrameTimerGPUAverage();
         float batchTime = getGPUContext()->getFrameTimerBatchAverage();
         auto lodManager = DependencyManager::get<LODManager>();
@@ -5897,8 +5980,8 @@ void Application::updateThreads(float deltaTime) {
 
     // parse voxel packets
     if (!_enableProcessOctreeThread) {
-        _octreeProcessor.threadRoutine();
-        _entityEditSender.threadRoutine();
+        _octreeProcessor->threadRoutine();
+        _entityEditSender->threadRoutine();
     }
 }
 
@@ -6021,7 +6104,7 @@ void Application::resetPhysicsReadyInformation() {
     _gpuTextureMemSizeStabilityCount = 0;
     _gpuTextureMemSizeAtLastCheck = 0;
     _physicsEnabled = false;
-    _octreeProcessor.stopSafeLanding();
+    _octreeProcessor->stopSafeLanding();
 }
 
 void Application::reloadResourceCaches() {
@@ -6166,7 +6249,7 @@ void Application::updateSecondaryCameraViewFrustum() {
     // camera should be.
 
     // Code based on SecondaryCameraJob
-    auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration();
+    auto renderConfig = _graphicsEngine->getRenderEngine()->getConfiguration();
     assert(renderConfig);
     auto camera = dynamic_cast<SecondaryCameraJobConfig*>(renderConfig->getConfig("SecondaryCamera"));
 
@@ -6284,7 +6367,7 @@ void Application::tryToEnablePhysics() {
         auto myAvatar = getMyAvatar();
         if (myAvatar->isReadyForPhysics()) {
             myAvatar->getCharacterController()->setPhysicsEngine(_physicsEngine);
-            _octreeProcessor.resetSafeLanding();
+            _octreeProcessor->resetSafeLanding();
             _physicsEnabled = true;
             setIsInterstitialMode(false);
             myAvatar->updateMotionBehaviorFromMenu();
@@ -6293,7 +6376,7 @@ void Application::tryToEnablePhysics() {
 }
 
 void Application::update(float deltaTime) {
-    PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_graphicsEngine._renderFrameCount + 1);
+    PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_graphicsEngine->_renderFrameCount + 1);
 
     if (_aboutToQuit) {
         return;
@@ -6310,12 +6393,12 @@ void Application::update(float deltaTime) {
         if (isServerlessMode()) {
             tryToEnablePhysics();
         } else if (_failedToConnectToEntityServer) {
-            if (_octreeProcessor.safeLandingIsActive()) {
-                _octreeProcessor.stopSafeLanding();
+            if (_octreeProcessor->safeLandingIsActive()) {
+                _octreeProcessor->stopSafeLanding();
             }
         } else {
-            _octreeProcessor.updateSafeLanding();
-            if (_octreeProcessor.safeLandingIsComplete()) {
+            _octreeProcessor->updateSafeLanding();
+            if (_octreeProcessor->safeLandingIsComplete()) {
                 tryToEnablePhysics();
             }
         }
@@ -6802,7 +6885,7 @@ void Application::update(float deltaTime) {
 }
 
 void Application::updateRenderArgs(float deltaTime) {
-    _graphicsEngine.editRenderArgs([this, deltaTime](AppRenderArgs& appRenderArgs) {
+    _graphicsEngine->editRenderArgs([this, deltaTime](AppRenderArgs& appRenderArgs) {
         PerformanceTimer perfTimer("editRenderArgs");
         appRenderArgs._headPose = getHMDSensorPose();
 
@@ -6831,7 +6914,7 @@ void Application::updateRenderArgs(float deltaTime) {
                 _viewFrustum.setProjection(adjustedProjection);
                 _viewFrustum.calculate();
             }
-            appRenderArgs._renderArgs = RenderArgs(_graphicsEngine.getGPUContext(), lodManager->getVisibilityDistance(),
+            appRenderArgs._renderArgs = RenderArgs(_graphicsEngine->getGPUContext(), lodManager->getVisibilityDistance(),
                 lodManager->getBoundaryLevelAdjust(), lodManager->getLODFarHalfAngleTan(), lodManager->getLODNearHalfAngleTan(),
                 lodManager->getLODFarDistance(), lodManager->getLODNearDistance(), RenderArgs::DEFAULT_RENDER_MODE,
                 RenderArgs::MONO, RenderArgs::DEFERRED, RenderArgs::RENDER_DEBUG_NONE);
@@ -6970,7 +7053,7 @@ int Application::sendNackPackets() {
 
             // if there are octree packets from this node that are waiting to be processed,
             // don't send a NACK since the missing packets may be among those waiting packets.
-            if (_octreeProcessor.hasPacketsToProcessFrom(nodeUUID)) {
+            if (_octreeProcessor->hasPacketsToProcessFrom(nodeUUID)) {
                 return;
             }
 
@@ -7012,7 +7095,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType) {
 
     const bool isModifiedQuery = !_physicsEnabled;
     if (isModifiedQuery) {
-        if (!_octreeProcessor.safeLandingIsActive()) {
+        if (!_octreeProcessor->safeLandingIsActive()) {
             // don't send the octreeQuery until SafeLanding knows it has started
             return;
         }
@@ -7281,12 +7364,12 @@ void Application::resettingDomain() {
 void Application::nodeAdded(SharedNodePointer node) {
     if (node->getType() == NodeType::EntityServer) {
         if (_failedToConnectToEntityServer && !_entityServerConnectionTimer.isActive()) {
-            _octreeProcessor.stopSafeLanding();
+            _octreeProcessor->stopSafeLanding();
             _failedToConnectToEntityServer = false;
         } else if (_entityServerConnectionTimer.isActive()) {
             _entityServerConnectionTimer.stop();
         }
-        _octreeProcessor.startSafeLanding();
+        _octreeProcessor->startSafeLanding();
         _entityServerConnectionTimer.setInterval(ENTITY_SERVER_CONNECTION_TIMEOUT);
         _entityServerConnectionTimer.start();
     }
@@ -7358,9 +7441,9 @@ void Application::nodeKilled(SharedNodePointer node) {
     // OctreePacketProcessor::nodeKilled is not being called when NodeList::nodeKilled is emitted.
     // This may have to do with GenericThread::threadRoutine() blocking the QThread event loop
 
-    _octreeProcessor.nodeKilled(node);
+    _octreeProcessor->nodeKilled(node);
 
-    _entityEditSender.nodeKilled(node);
+    _entityEditSender->nodeKilled(node);
 
     if (node->getType() == NodeType::AudioMixer) {
         QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(), "audioMixerKilled");
@@ -7449,7 +7532,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptManagerPoint
     // setup the packet sender of the script engine's scripting interfaces so
     // we can use the same ones from the application.
     auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
-    entityScriptingInterface->setPacketSender(&_entityEditSender);
+    entityScriptingInterface->setPacketSender(_entityEditSender.get());
     entityScriptingInterface->setEntityTree(getEntities()->getTree());
 
     if (property(hifi::properties::TEST).isValid()) {
@@ -7575,7 +7658,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptManagerPoint
 
     {
         auto connection = std::make_shared<QMetaObject::Connection>();
-        *connection = scriptManager->connect(scriptManager.get(), &ScriptManager::scriptEnding, [scriptManager, connection]() {
+        *connection = scriptManager->connect(scriptManager.get(), &ScriptManager::scriptEnding, [this, scriptManager, connection]() {
             // Request removal of controller routes with callbacks to a given script engine
             auto userInputMapper = DependencyManager::get<UserInputMapper>();
             // scheduleScriptEndpointCleanup will have the last instance of shared pointer to script manager
@@ -8215,7 +8298,7 @@ void Application::addAssetToWorldCheckModelSize() {
         propertyFlags += PROP_NAME;
         propertyFlags += PROP_DIMENSIONS;
         auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
-        auto properties = entityScriptingInterface->getEntityPropertiesInternal(entityID, propertyFlags);
+        auto properties = entityScriptingInterface->getEntityPropertiesInternal(entityID, propertyFlags, false);
         auto name = properties.getName();
         auto dimensions = properties.getDimensions();
 
@@ -8763,26 +8846,6 @@ void Application::sendLambdaEvent(const std::function<void()>& f) {
     }
 }
 
-void Application::initPlugins(const QCommandLineParser& parser) {
-    if (parser.isSet("display")) {
-        auto preferredDisplays = parser.value("display").split(',', Qt::SkipEmptyParts);
-        qInfo() << "Setting prefered display plugins:" << preferredDisplays;
-        PluginManager::getInstance()->setPreferredDisplayPlugins(preferredDisplays);
-    }
-
-    if (parser.isSet("disable-displays")) {
-        auto disabledDisplays = parser.value("disable-displays").split(',', Qt::SkipEmptyParts);
-        qInfo() << "Disabling following display plugins:"  << disabledDisplays;
-        PluginManager::getInstance()->disableDisplays(disabledDisplays);
-    }
-
-    if (parser.isSet("disable-inputs")) {
-        auto disabledInputs = parser.value("disable-inputs").split(',', Qt::SkipEmptyParts);
-        qInfo() << "Disabling following input plugins:" << disabledInputs;
-        PluginManager::getInstance()->disableInputs(disabledInputs);
-    }
-}
-
 void Application::shutdownPlugins() {
 }
 
@@ -9205,7 +9268,7 @@ void Application::updateLoginDialogPosition() {
     auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
     EntityPropertyFlags desiredProperties;
     desiredProperties += PROP_POSITION;
-    auto properties = entityScriptingInterface->getEntityPropertiesInternal(_loginDialogID, desiredProperties);
+    auto properties = entityScriptingInterface->getEntityPropertiesInternal(_loginDialogID, desiredProperties, false);
     auto positionVec = properties.getPosition();
     auto cameraPositionVec = _myCamera.getPosition();
     auto cameraOrientation = cancelOutRollAndPitch(_myCamera.getOrientation());
diff --git a/interface/src/Application.h b/interface/src/Application.h
index 82b39e868b..63a035dd45 100644
--- a/interface/src/Application.h
+++ b/interface/src/Application.h
@@ -123,6 +123,31 @@ class Application : public QApplication,
     friend class OctreePacketProcessor;
 
 public:
+
+    /**
+     * @brief Initialize the plugin manager
+     *
+     * This both does the initial startup and parses arguments. This
+     * is necessary because the plugin manager's options must be set
+     * before any usage of it is made, or they won't apply.
+     *
+     * @param parser
+     */
+    void initializePluginManager(const QCommandLineParser& parser);
+
+    /**
+     * @brief Initialize everything
+     *
+     * This is a QApplication, and for Qt reasons it's desirable to create this object
+     * as early as possible. Without that some Qt functions don't work, like QCoreApplication::applicationDirPath()
+     *
+     * So we keep the constructor as minimal as possible, and do the rest of the work in
+     * this function.
+     */
+    void initialize(const QCommandLineParser &parser);
+
+    void setPreviousSessionCrashed(bool value) { _previousSessionCrashed = value; }
+
     // virtual functions required for PluginContainer
     virtual ui::Menu* getPrimaryMenu() override;
     virtual void requestReset() override { resetSensors(false); }
@@ -135,15 +160,12 @@ public:
 
     virtual DisplayPluginPointer getActiveDisplayPlugin() const override;
 
-    // FIXME? Empty methods, do we still need them?
-    static void initPlugins(const QCommandLineParser& parser);
     static void shutdownPlugins();
 
     Application(
         int& argc, char** argv,
         const QCommandLineParser& parser,
-        QElapsedTimer& startup_time,
-        bool runningMarkerExisted
+        QElapsedTimer& startup_time
     );
     ~Application();
 
@@ -197,16 +219,16 @@ public:
 
     const ConicalViewFrustums& getConicalViews() const override { return _conicalViews; }
 
-    const OctreePacketProcessor& getOctreePacketProcessor() const { return _octreeProcessor; }
+    const OctreePacketProcessor& getOctreePacketProcessor() const { return *_octreeProcessor; }
     QSharedPointer<EntityTreeRenderer> getEntities() const { return DependencyManager::get<EntityTreeRenderer>(); }
     MainWindow* getWindow() const { return _window; }
     EntityTreePointer getEntityClipboard() const { return _entityClipboard; }
-    EntityEditPacketSender* getEntityEditPacketSender() { return &_entityEditSender; }
+    std::shared_ptr<EntityEditPacketSender> getEntityEditPacketSender() { return _entityEditSender; }
 
     ivec2 getMouse() const;
 
-    ApplicationOverlay& getApplicationOverlay() { return _applicationOverlay; }
-    const ApplicationOverlay& getApplicationOverlay() const { return _applicationOverlay; }
+    ApplicationOverlay& getApplicationOverlay() { return *_applicationOverlay; }
+    const ApplicationOverlay& getApplicationOverlay() const { return *_applicationOverlay; }
     CompositorHelper& getApplicationCompositor() const;
 
     Overlays& getOverlays() { return _overlays; }
@@ -214,8 +236,8 @@ public:
     PerformanceManager& getPerformanceManager() { return _performanceManager; }
     RefreshRateManager& getRefreshRateManager() { return _refreshRateManager; }
 
-    size_t getRenderFrameCount() const { return _graphicsEngine.getRenderFrameCount(); }
-    float getRenderLoopRate() const { return _graphicsEngine.getRenderLoopRate(); }
+    size_t getRenderFrameCount() const { return _graphicsEngine->getRenderFrameCount(); }
+    float getRenderLoopRate() const { return _graphicsEngine->getRenderLoopRate(); }
     float getNumCollisionObjects() const;
     float getTargetRenderFrameRate() const; // frames/second
 
@@ -293,9 +315,9 @@ public:
     void setMaxOctreePacketsPerSecond(int maxOctreePPS);
     int getMaxOctreePacketsPerSecond() const;
 
-    render::ScenePointer getMain3DScene() override { return _graphicsEngine.getRenderScene(); }
-    render::EnginePointer getRenderEngine() override { return  _graphicsEngine.getRenderEngine(); }
-    gpu::ContextPointer getGPUContext() const { return _graphicsEngine.getGPUContext(); }
+    render::ScenePointer getMain3DScene() override { return _graphicsEngine->getRenderScene(); }
+    render::EnginePointer getRenderEngine() override { return  _graphicsEngine->getRenderEngine(); }
+    gpu::ContextPointer getGPUContext() const { return _graphicsEngine->getGPUContext(); }
 
     const GameWorkload& getGameWorkload() const { return _gameWorkload; }
 
@@ -709,8 +731,8 @@ private:
     bool _enableProcessOctreeThread;
     bool _interstitialMode { false };
 
-    OctreePacketProcessor _octreeProcessor;
-    EntityEditPacketSender _entityEditSender;
+    std::shared_ptr<OctreePacketProcessor> _octreeProcessor;
+    std::shared_ptr<EntityEditPacketSender> _entityEditSender;
 
     StDev _idleLoopStdev;
     float _idleLoopMeasuredJitter;
@@ -757,13 +779,13 @@ private:
 
     GameWorkload _gameWorkload;
 
-    GraphicsEngine _graphicsEngine;
+    std::shared_ptr<GraphicsEngine> _graphicsEngine;
     void updateRenderArgs(float deltaTime);
 
     bool _disableLoginScreen { true };
 
     Overlays _overlays;
-    ApplicationOverlay _applicationOverlay;
+    std::shared_ptr<ApplicationOverlay> _applicationOverlay;
     OverlayConductor _overlayConductor;
 
     DialogsManagerScriptingInterface* _dialogsManagerScriptingInterface = new DialogsManagerScriptingInterface();
@@ -860,5 +882,7 @@ private:
     bool _crashOnShutdown { false };
 
     DiscordPresence* _discordPresence{ nullptr };
+
+    bool _profilingInitialized { false };
 };
 #endif // hifi_Application_h
diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp
index 256ce2f6fc..461c55e64e 100644
--- a/interface/src/AvatarBookmarks.cpp
+++ b/interface/src/AvatarBookmarks.cpp
@@ -41,6 +41,15 @@
 #include <QtQuick/QQuickWindow>
 #include <memory>
 #include "WarningsSuppression.h"
+#include "ScriptPermissions.h"
+
+QVariantMap AvatarBookmarks::getBookmarks() {
+    if (ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) {
+        return _bookmarks;
+    } else {
+        return {};
+    }
+}
 
 void addAvatarEntities(const QVariantList& avatarEntities) {
     auto nodeList = DependencyManager::get<NodeList>();
@@ -123,6 +132,12 @@ AvatarBookmarks::AvatarBookmarks() {
 }
 
 void AvatarBookmarks::addBookmark(const QString& bookmarkName) {
+    if (ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) {
+        addBookmarkInternal(bookmarkName);
+    }
+}
+
+void AvatarBookmarks::addBookmarkInternal(const QString& bookmarkName) {
     if (QThread::currentThread() != thread()) {
         BLOCKING_INVOKE_METHOD(this, "addBookmark", Q_ARG(QString, bookmarkName));
         return;
@@ -134,6 +149,12 @@ void AvatarBookmarks::addBookmark(const QString& bookmarkName) {
 }
 
 void AvatarBookmarks::saveBookmark(const QString& bookmarkName) {
+    if (ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) {
+        saveBookmarkInternal(bookmarkName);
+    }
+}
+
+void AvatarBookmarks::saveBookmarkInternal(const QString& bookmarkName) {
     if (QThread::currentThread() != thread()) {
         BLOCKING_INVOKE_METHOD(this, "saveBookmark", Q_ARG(QString, bookmarkName));
         return;
@@ -145,6 +166,12 @@ void AvatarBookmarks::saveBookmark(const QString& bookmarkName) {
 }
 
 void AvatarBookmarks::removeBookmark(const QString& bookmarkName) {
+    if (ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) {
+        removeBookmarkInternal(bookmarkName);
+    }
+}
+
+void AvatarBookmarks::removeBookmarkInternal(const QString& bookmarkName) {
     if (QThread::currentThread() != thread()) {
         BLOCKING_INVOKE_METHOD(this, "removeBookmark", Q_ARG(QString, bookmarkName));
         return;
@@ -200,6 +227,12 @@ void AvatarBookmarks::updateAvatarEntities(const QVariantList &avatarEntities) {
  */
 
 void AvatarBookmarks::loadBookmark(const QString& bookmarkName) {
+    if (ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) {
+        loadBookmarkInternal(bookmarkName);
+    }
+}
+
+void AvatarBookmarks::loadBookmarkInternal(const QString& bookmarkName) {
     if (QThread::currentThread() != thread()) {
         BLOCKING_INVOKE_METHOD(this, "loadBookmark", Q_ARG(QString, bookmarkName));
         return;
@@ -268,6 +301,15 @@ void AvatarBookmarks::readFromFile() {
 }
 
 QVariantMap AvatarBookmarks::getBookmark(const QString &bookmarkName)
+{
+    if (ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) {
+        return getBookmarkInternal(bookmarkName);
+    } else {
+        return {};
+    }
+}
+
+QVariantMap AvatarBookmarks::getBookmarkInternal(const QString &bookmarkName)
 {
     if (QThread::currentThread() != thread()) {
         QVariantMap result;
diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h
index c2c7eb5a0a..bf06743b3f 100644
--- a/interface/src/AvatarBookmarks.h
+++ b/interface/src/AvatarBookmarks.h
@@ -100,7 +100,7 @@ public slots:
      *     print("- " + key + " " + bookmarks[key].avatarUrl);
      * };
      */
-    QVariantMap getBookmarks() { return _bookmarks; }
+    QVariantMap getBookmarks();
 
 signals:
     /*@jsdoc
@@ -147,6 +147,11 @@ protected slots:
     void deleteBookmark() override;
 
 private:
+    QVariantMap getBookmarkInternal(const QString &bookmarkName);
+    void addBookmarkInternal(const QString& bookmarkName);
+    void saveBookmarkInternal(const QString& bookmarkName);
+    void loadBookmarkInternal(const QString& bookmarkName);
+    void removeBookmarkInternal(const QString& bookmarkName);
     const QString AVATARBOOKMARKS_FILENAME = "avatarbookmarks.json";
     const QString ENTRY_AVATAR_URL = "avatarUrl";
     const QString ENTRY_AVATAR_ICON = "avatarIcon";
diff --git a/interface/src/CrashRecoveryHandler.cpp b/interface/src/CrashRecoveryHandler.cpp
index 1f6cbef9ba..c03e8bc70f 100644
--- a/interface/src/CrashRecoveryHandler.cpp
+++ b/interface/src/CrashRecoveryHandler.cpp
@@ -258,10 +258,11 @@ void CrashRecoveryHandler::handleCrash(CrashRecoveryHandler::Action action) {
         // Display name and avatar
         settings.beginGroup(AVATAR_GROUP);
         displayName = settings.value(DISPLAY_NAME_KEY).toString();
-        fullAvatarURL = settings.value(FULL_AVATAR_URL_KEY).toUrl();
         fullAvatarModelName = settings.value(FULL_AVATAR_MODEL_NAME_KEY).toString();
         settings.endGroup();
 
+        fullAvatarURL = settings.value(SETTINGS_FULL_PRIVATE_GROUP_NAME + "/" + AVATAR_GROUP + "/" + FULL_AVATAR_URL_KEY).toUrl();
+
         // Tutorial complete
         tutorialComplete = settings.value(TUTORIAL_COMPLETE_FLAG_KEY).toBool();
     }
@@ -280,12 +281,12 @@ void CrashRecoveryHandler::handleCrash(CrashRecoveryHandler::Action action) {
         // Display name and avatar
         settings.beginGroup(AVATAR_GROUP);
         settings.setValue(DISPLAY_NAME_KEY, displayName);
-        settings.setValue(FULL_AVATAR_URL_KEY, fullAvatarURL);
         settings.setValue(FULL_AVATAR_MODEL_NAME_KEY, fullAvatarModelName);
         settings.endGroup();
 
+        settings.setValue(SETTINGS_FULL_PRIVATE_GROUP_NAME + "/" + AVATAR_GROUP + "/" + FULL_AVATAR_URL_KEY, fullAvatarURL);
+
         // Tutorial complete
         settings.setValue(TUTORIAL_COMPLETE_FLAG_KEY, tutorialComplete);
     }
 }
-
diff --git a/interface/src/LODManager.cpp b/interface/src/LODManager.cpp
index fdd1b51fb3..9512b2f758 100644
--- a/interface/src/LODManager.cpp
+++ b/interface/src/LODManager.cpp
@@ -49,10 +49,10 @@ LODManager::LODManager() {
 const float LOD_ADJUST_RUNNING_AVG_TIMESCALE = 0.08f; // sec
 
 // batchTIme is always contained in presentTime.
-// We favor using batchTime instead of presentTime as a representative value for rendering duration (on present thread) 
+// We favor using batchTime instead of presentTime as a representative value for rendering duration (on present thread)
 // if batchTime + cushionTime < presentTime.
 // since we are shooting for fps around 60, 90Hz, the ideal frames are around 10ms
-// so we are picking a cushion time of 3ms 
+// so we are picking a cushion time of 3ms
 const float LOD_BATCH_TO_PRESENT_CUSHION_TIME = 3.0f; // msec
 
 void LODManager::setRenderTimes(float presentTime, float engineRunTime, float batchTime, float gpuTime) {
@@ -64,8 +64,8 @@ void LODManager::setRenderTimes(float presentTime, float engineRunTime, float ba
 }
 
 void LODManager::autoAdjustLOD(float realTimeDelta) {
-    std::lock_guard<std::mutex> { _automaticLODLock };
- 
+    std::lock_guard<std::mutex> lock(_automaticLODLock);
+
     // The "render time" is the worse of:
     // - engineRunTime: Time spent in the render thread in the engine producing the gpu::Frame N
     // - batchTime: Time spent in the present thread processing the batches of gpu::Frame N+1
@@ -92,7 +92,7 @@ void LODManager::autoAdjustLOD(float realTimeDelta) {
     float smoothBlend = (realTimeDelta <  LOD_ADJUST_RUNNING_AVG_TIMESCALE * _smoothScale) ? realTimeDelta / (LOD_ADJUST_RUNNING_AVG_TIMESCALE * _smoothScale) : 1.0f;
 
     //Evaluate the running averages for the render time
-    // We must sanity check for the output average evaluated to be in a valid range to avoid issues 
+    // We must sanity check for the output average evaluated to be in a valid range to avoid issues
     _nowRenderTime = (1.0f - nowBlend) * _nowRenderTime + nowBlend * maxRenderTime; // msec
     _nowRenderTime = std::max(0.0f, std::min(_nowRenderTime, (float)MSECS_PER_SECOND));
     _smoothRenderTime = (1.0f - smoothBlend) * _smoothRenderTime + smoothBlend * maxRenderTime; // msec
@@ -112,7 +112,7 @@ void LODManager::autoAdjustLOD(float realTimeDelta) {
     // Current fps based on latest measurments
     float currentNowFPS = (float)MSECS_PER_SECOND / _nowRenderTime;
     float currentSmoothFPS = (float)MSECS_PER_SECOND / _smoothRenderTime;
- 
+
     // Compute the Variance of the FPS signal (FPS - smouthFPS)^2
     // Also scale it by a percentage for fine tuning (default is 100%)
     float currentVarianceFPS = (currentSmoothFPS - currentNowFPS);
@@ -165,7 +165,7 @@ void LODManager::autoAdjustLOD(float realTimeDelta) {
 
     // Compute the output of the PID and record intermediate results for tuning
     _pidOutputs.x = _pidCoefs.x * error;        // Kp * error
-    _pidOutputs.y = _pidCoefs.y * integral;     // Ki * integral 
+    _pidOutputs.y = _pidCoefs.y * integral;     // Ki * integral
     _pidOutputs.z = _pidCoefs.z * derivative;   // Kd * derivative
 
     auto output = _pidOutputs.x + _pidOutputs.y + _pidOutputs.z;
@@ -300,7 +300,7 @@ void LODManager::resetLODAdjust() {
 }
 
 void LODManager::setAutomaticLODAdjust(bool value) {
-    std::lock_guard<std::mutex> { _automaticLODLock };
+    std::lock_guard<std::mutex> lock(_automaticLODLock);
     _automaticLODAdjust = value;
     saveSettings();
     emit autoLODChanged();
@@ -399,7 +399,7 @@ void LODManager::loadSettings() {
     if (qApp->property(hifi::properties::OCULUS_STORE).toBool() && firstRun.get()) {
         hmdQuality = WORLD_DETAIL_HIGH;
     }
-    
+
     _automaticLODAdjust = automaticLODAdjust.get();
     _lodHalfAngle = lodHalfAngle.get();
 
@@ -457,7 +457,7 @@ float LODManager::getLODTargetFPS() const {
     if (qApp->isHMDMode()) {
         lodTargetFPS = getHMDLODTargetFPS();
     }
-    
+
     // if RefreshRate is slower than LOD target then it becomes the true LOD target
     if (lodTargetFPS > refreshRateFPS) {
         return refreshRateFPS;
@@ -476,7 +476,7 @@ void LODManager::setWorldDetailQuality(WorldDetailQuality quality, bool isHMDMod
         setDesktopLODTargetFPS(desiredFPS);
     }
 }
-    
+
 void LODManager::setWorldDetailQuality(WorldDetailQuality quality) {
     setWorldDetailQuality(quality, qApp->isHMDMode());
     saveSettings();
@@ -492,7 +492,7 @@ ScriptValue worldDetailQualityToScriptValue(ScriptEngine* engine, const WorldDet
 }
 
 bool worldDetailQualityFromScriptValue(const ScriptValue& object, WorldDetailQuality& worldDetailQuality) {
-    worldDetailQuality = 
+    worldDetailQuality =
         static_cast<WorldDetailQuality>(std::min(std::max(object.toInt32(), (int)WORLD_DETAIL_LOW), (int)WORLD_DETAIL_HIGH));
     return true;
 }
diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp
index d30fe66a6f..7017f2a083 100644
--- a/interface/src/Menu.cpp
+++ b/interface/src/Menu.cpp
@@ -323,6 +323,19 @@ Menu::Menu() {
         }
     });
 
+    // Settings > Script Security
+    action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::ScriptSecurity);
+    connect(action, &QAction::triggered, [] {
+        auto tablet = DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system");
+        auto hmd = DependencyManager::get<HMDScriptingInterface>();
+
+        tablet->pushOntoStack("hifi/dialogs/security/ScriptSecurity.qml");
+
+        if (!hmd->getShouldShowTablet()) {
+            hmd->toggleShouldShowTablet();
+        }
+    });
+
     // Settings > Developer Menu
     addCheckableActionToQMenuAndActionHash(settingsMenu, "Developer Menu", 0, false, this, SLOT(toggleDeveloperMenus()));
 
@@ -388,13 +401,18 @@ Menu::Menu() {
 
     // Developer > UI >>>
     MenuWrapper* uiOptionsMenu = developerMenu->addMenu("UI");
+
+    // Developer > UI > Show Overlays
+    action = addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::Overlays, 0, true);
+
+    connect(action, &QAction::triggered, [action] {
+        qApp->getApplicationOverlay().setEnabled(action->isChecked());
+    });
+
+    // Developer > UI > Desktop Tablet Becomes Toolbar
     action = addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::DesktopTabletToToolbar, 0,
                                                     qApp->getDesktopTabletBecomesToolbarSetting());
 
-    // Developer > UI > Show Overlays
-    addCheckableActionToQMenuAndActionHash(uiOptionsMenu, MenuOption::Overlays, 0, true);
-
-    // Developer > UI > Desktop Tablet Becomes Toolbar
     connect(action, &QAction::triggered, [action] {
         qApp->setDesktopTabletBecomesToolbarSetting(action->isChecked());
     });
@@ -445,6 +463,10 @@ Menu::Menu() {
     textureGroup->addAction(addCheckableActionToQMenuAndActionHash(textureMenu, MenuOption::RenderMaxTexture4096MB, 0, false));
     textureGroup->addAction(addCheckableActionToQMenuAndActionHash(textureMenu, MenuOption::RenderMaxTexture6144MB, 0, false));
     textureGroup->addAction(addCheckableActionToQMenuAndActionHash(textureMenu, MenuOption::RenderMaxTexture8192MB, 0, false));
+    textureGroup->addAction(addCheckableActionToQMenuAndActionHash(textureMenu, MenuOption::RenderMaxTexture10240MB, 0, false));
+    textureGroup->addAction(addCheckableActionToQMenuAndActionHash(textureMenu, MenuOption::RenderMaxTexture12288MB, 0, false));
+    textureGroup->addAction(addCheckableActionToQMenuAndActionHash(textureMenu, MenuOption::RenderMaxTexture16384MB, 0, false));
+    textureGroup->addAction(addCheckableActionToQMenuAndActionHash(textureMenu, MenuOption::RenderMaxTexture20480MB, 0, false));
     connect(textureGroup, &QActionGroup::triggered, [textureGroup] {
         auto checked = textureGroup->checkedAction();
         auto text = checked->text();
@@ -465,8 +487,16 @@ Menu::Menu() {
             newMaxTextureMemory = MB_TO_BYTES(4096);
         } else if (MenuOption::RenderMaxTexture6144MB == text) {
             newMaxTextureMemory = MB_TO_BYTES(6144);
-        } else if (MenuOption::RenderMaxTexture8192MB == text) {
+        } else if (MenuOption::RenderMaxTexture1024MB == text) {
             newMaxTextureMemory = MB_TO_BYTES(8192);
+        } else if (MenuOption::RenderMaxTexture10240MB == text) {
+            newMaxTextureMemory = MB_TO_BYTES(10240);
+        } else if (MenuOption::RenderMaxTexture12288MB == text) {
+            newMaxTextureMemory = MB_TO_BYTES(12288);
+        } else if (MenuOption::RenderMaxTexture16384MB == text) {
+            newMaxTextureMemory = MB_TO_BYTES(16384);
+        } else if (MenuOption::RenderMaxTexture20480MB == text) {
+            newMaxTextureMemory = MB_TO_BYTES(20480);
         }
         gpu::Texture::setAllowedGPUMemoryUsage(newMaxTextureMemory);
     });
@@ -775,7 +805,6 @@ Menu::Menu() {
         addActionToQMenuAndActionHash(crashMenu, MenuOption::CrashOnShutdown, 0, qApp, SLOT(crashOnShutdown()));
     }
 
-
     // Developer > Show Statistics
     addCheckableActionToQMenuAndActionHash(developerMenu, MenuOption::Stats, 0, true);
 
diff --git a/interface/src/Menu.h b/interface/src/Menu.h
index 2bb9f10e68..e0cdfdf4fd 100644
--- a/interface/src/Menu.h
+++ b/interface/src/Menu.h
@@ -175,6 +175,10 @@ namespace MenuOption {
     const QString RenderMaxTexture4096MB = "4096 MB";
     const QString RenderMaxTexture6144MB = "6144 MB";
     const QString RenderMaxTexture8192MB = "8192 MB";
+    const QString RenderMaxTexture10240MB = "10240 MB";
+    const QString RenderMaxTexture12288MB = "12288 MB";
+    const QString RenderMaxTexture16384MB = "16384 MB";
+    const QString RenderMaxTexture20480MB = "20480 MB";
     const QString RenderSensorToWorldMatrix = "Show SensorToWorld Matrix";
     const QString RenderIKTargets = "Show IK Targets";
     const QString RenderIKConstraints = "Show IK Constraints";
@@ -186,6 +190,7 @@ namespace MenuOption {
     const QString RunTimingTests = "Run Timing Tests";
     const QString ScriptedMotorControl = "Enable Scripted Motor Control";
     const QString EntityScriptQMLWhitelist = "Entity Script / QML Whitelist";
+    const QString ScriptSecurity = "Script Security";
     const QString ShowTrackedObjects = "Show Tracked Objects";
     const QString SelfieCamera = "Selfie";
     const QString SendWrongDSConnectVersion = "Send wrong DS connect version";
diff --git a/interface/src/RefreshRateManager.cpp b/interface/src/RefreshRateManager.cpp
index 09428da5ea..9ba446750b 100644
--- a/interface/src/RefreshRateManager.cpp
+++ b/interface/src/RefreshRateManager.cpp
@@ -48,12 +48,13 @@ static const int VR_TARGET_RATE = 90;
  *     <tr><td><code>"Interactive"</code></td><td>Medium refresh rate, which is reduced when Interface doesn't have focus or is 
  *         minimized.</td></tr>
  *     <tr><td><code>"Realtime"</code></td><td>High refresh rate, even when Interface doesn't have focus or is minimized.
+ *     <tr><td><code>"Custom"</code></td><td>Custom refresh rate for full control over the refresh rate in all states.
  *   </tbody>
  * </table>
  * @typedef {string} RefreshRateProfileName
  */
 static const std::array<std::string, RefreshRateManager::RefreshRateProfile::PROFILE_NUM> REFRESH_RATE_PROFILE_TO_STRING =
-    { { "Eco", "Interactive", "Realtime" } };
+    { { "Eco", "Interactive", "Realtime", "Custom" } };
 
 /*@jsdoc
  * <p>Interface states that affect the refresh rate.</p>
@@ -94,7 +95,8 @@ static const std::array<std::string, RefreshRateManager::UXMode::UX_NUM> UX_MODE
 static const std::map<std::string, RefreshRateManager::RefreshRateProfile> REFRESH_RATE_PROFILE_FROM_STRING =
     { { "Eco", RefreshRateManager::RefreshRateProfile::ECO },
       { "Interactive", RefreshRateManager::RefreshRateProfile::INTERACTIVE },
-      { "Realtime", RefreshRateManager::RefreshRateProfile::REALTIME } };
+      { "Realtime", RefreshRateManager::RefreshRateProfile::REALTIME },
+      { "Custom", RefreshRateManager::RefreshRateProfile::CUSTOM } };
 
 
 // Porfile regimes are:
@@ -107,10 +109,12 @@ static const std::array<int, RefreshRateManager::RefreshRateRegime::REGIME_NUM>
     { { 30, 20, 10, 2, 30, 30 } };
 
 static const std::array<int, RefreshRateManager::RefreshRateRegime::REGIME_NUM> REALTIME_PROFILE =
-    { { 60, 60, 60, 2, 30, 30} };
+    { { 60, 60, 60, 2, 30, 30 } };
 
-static const std::array<std::array<int, RefreshRateManager::RefreshRateRegime::REGIME_NUM>, RefreshRateManager::RefreshRateProfile::PROFILE_NUM> REFRESH_RATE_PROFILES =
-    { { ECO_PROFILE, INTERACTIVE_PROFILE, REALTIME_PROFILE } };
+static const std::array<int, RefreshRateManager::RefreshRateRegime::REGIME_NUM> CUSTOM_PROFILE = REALTIME_PROFILE; // derived from settings and modified by scripts below
+
+static std::array<std::array<int, RefreshRateManager::RefreshRateRegime::REGIME_NUM>, RefreshRateManager::RefreshRateProfile::PROFILE_NUM> REFRESH_RATE_PROFILES =
+    { { ECO_PROFILE, INTERACTIVE_PROFILE, REALTIME_PROFILE, CUSTOM_PROFILE } };
 
 
 static const int INACTIVE_TIMER_LIMIT = 3000;
@@ -134,6 +138,10 @@ std::string RefreshRateManager::uxModeToString(RefreshRateManager::RefreshRateMa
 
 RefreshRateManager::RefreshRateManager() {
     _refreshRateProfile = (RefreshRateManager::RefreshRateProfile) _refreshRateProfileSetting.get();
+    for (size_t i = 0; i < _customRefreshRateSettings.size(); i++) {
+        REFRESH_RATE_PROFILES[CUSTOM][i] = _customRefreshRateSettings[i].get();
+    }
+
     _inactiveTimer->setInterval(INACTIVE_TIMER_LIMIT);
     _inactiveTimer->setSingleShot(true);
     QObject::connect(_inactiveTimer.get(), &QTimer::timeout, [&] {
@@ -168,6 +176,25 @@ void RefreshRateManager::setRefreshRateProfile(RefreshRateManager::RefreshRatePr
     }
 }
 
+int RefreshRateManager::getCustomRefreshRate(RefreshRateRegime regime) {
+    if (isValidRefreshRateRegime(regime)) {
+        return REFRESH_RATE_PROFILES[RefreshRateProfile::CUSTOM][regime];
+    }
+
+    return 0;
+}
+
+void RefreshRateManager::setCustomRefreshRate(RefreshRateRegime regime, int value) {
+    value = std::max(value, 1);
+    if (isValidRefreshRateRegime(regime)) {
+        _refreshRateProfileSettingLock.withWriteLock([&] {
+            REFRESH_RATE_PROFILES[RefreshRateProfile::CUSTOM][regime] = value;
+            _customRefreshRateSettings[regime].set(value);
+        });
+        updateRefreshRateController();
+    }
+}
+
 RefreshRateManager::RefreshRateProfile RefreshRateManager::getRefreshRateProfile() const {
     RefreshRateManager::RefreshRateProfile profile = RefreshRateManager::RefreshRateProfile::REALTIME;
 
@@ -191,7 +218,6 @@ void RefreshRateManager::setRefreshRateRegime(RefreshRateManager::RefreshRateReg
         _refreshRateRegime = refreshRateRegime;
         updateRefreshRateController();
     }
-
 }
 
 void RefreshRateManager::setUXMode(RefreshRateManager::UXMode uxMode) {
diff --git a/interface/src/RefreshRateManager.h b/interface/src/RefreshRateManager.h
index 4b91f0c45e..6b94a94a4a 100644
--- a/interface/src/RefreshRateManager.h
+++ b/interface/src/RefreshRateManager.h
@@ -32,10 +32,11 @@ public:
         ECO = 0,
         INTERACTIVE,
         REALTIME,
+        CUSTOM,
         PROFILE_NUM
     };
     Q_ENUM(RefreshRateProfile)
-    static bool isValidRefreshRateProfile(RefreshRateProfile value) { return (value >= RefreshRateProfile::ECO && value <= RefreshRateProfile::REALTIME); }
+    static bool isValidRefreshRateProfile(RefreshRateProfile value) { return (value >= 0 && value < RefreshRateProfile::PROFILE_NUM); }
 
     /*@jsdoc
      * <p>Interface states that affect the refresh rate.</p>
@@ -106,6 +107,9 @@ public:
     // query the refresh rate target at the specified combination
     int queryRefreshRateTarget(RefreshRateProfile profile, RefreshRateRegime regime, UXMode uxMode) const;
 
+    int getCustomRefreshRate(RefreshRateRegime regime);
+    void setCustomRefreshRate(RefreshRateRegime regime, int value);
+
     void resetInactiveTimer();
     void toggleInactive();
 
@@ -121,7 +125,15 @@ private:
     UXMode _uxMode { UXMode::DESKTOP };
 
     mutable ReadWriteLockable _refreshRateProfileSettingLock;
-    Setting::Handle<int> _refreshRateProfileSetting { "refreshRateProfile", RefreshRateProfile::INTERACTIVE };
+    Setting::Handle<int> _refreshRateProfileSetting{ "refreshRateProfile", RefreshRateProfile::INTERACTIVE };
+    std::array<Setting::Handle<int>, REGIME_NUM> _customRefreshRateSettings { {
+        { "customRefreshRateFocusActive", 60 },
+        { "customRefreshRateFocusInactive", 60 },
+        { "customRefreshRateUnfocus", 60 },
+        { "customRefreshRateMinimized", 2 },
+        { "customRefreshRateStartup", 30 },
+        { "customRefreshRateShutdown", 30 }
+    } };
 
     std::function<void(int)> _refreshRateOperator { nullptr };
 
diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp
index 5c2fc01911..a46652c7d9 100644
--- a/interface/src/avatar/AvatarDoctor.cpp
+++ b/interface/src/avatar/AvatarDoctor.cpp
@@ -333,6 +333,14 @@ void AvatarDoctor::diagnoseTextures() {
         addTextureToList(material.occlusionTexture);
         addTextureToList(material.scatteringTexture);
         addTextureToList(material.lightmapTexture);
+
+        if (material.isMToonMaterial) {
+            addTextureToList(material.shadeTexture);
+            addTextureToList(material.shadingShiftTexture);
+            addTextureToList(material.matcapTexture);
+            addTextureToList(material.rimTexture);
+            addTextureToList(material.uvAnimationTexture);
+        }
     }
 
     for (const auto& materialMapping : model->getMaterialMapping()) {
diff --git a/interface/src/avatar/AvatarMotionState.cpp b/interface/src/avatar/AvatarMotionState.cpp
index 97085c8443..362ac579a9 100644
--- a/interface/src/avatar/AvatarMotionState.cpp
+++ b/interface/src/avatar/AvatarMotionState.cpp
@@ -27,6 +27,10 @@ void AvatarMotionState::handleEasyChanges(uint32_t& flags) {
     if (flags & Simulation::DIRTY_PHYSICS_ACTIVATION && !_body->isActive()) {
         _body->activate();
     }
+
+    if (flags & Simulation::DIRTY_MASS) {
+        updateBodyMassProperties();
+    }
 }
 
 AvatarMotionState::~AvatarMotionState() {
diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp
index 466db613f6..cf41c0a040 100644
--- a/interface/src/avatar/AvatarProject.cpp
+++ b/interface/src/avatar/AvatarProject.cpp
@@ -136,6 +136,14 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder,
         addTextureToList(material.occlusionTexture);
         addTextureToList(material.scatteringTexture);
         addTextureToList(material.lightmapTexture);
+
+        if (material.isMToonMaterial) {
+            addTextureToList(material.shadeTexture);
+            addTextureToList(material.shadingShiftTexture);
+            addTextureToList(material.matcapTexture);
+            addTextureToList(material.rimTexture);
+            addTextureToList(material.uvAnimationTexture);
+        }
     }
 
     QDir textureDir(textureFolder.isEmpty() ? fbxInfo.absoluteDir() : textureFolder);
diff --git a/interface/src/avatar/DetailedMotionState.cpp b/interface/src/avatar/DetailedMotionState.cpp
index 02a2b9d425..a22d533bc9 100644
--- a/interface/src/avatar/DetailedMotionState.cpp
+++ b/interface/src/avatar/DetailedMotionState.cpp
@@ -31,6 +31,10 @@ void DetailedMotionState::handleEasyChanges(uint32_t& flags) {
     if (flags & Simulation::DIRTY_PHYSICS_ACTIVATION && !_body->isActive()) {
         _body->activate();
     }
+
+    if (flags & Simulation::DIRTY_MASS) {
+        updateBodyMassProperties();
+    }
 }
 
 DetailedMotionState::~DetailedMotionState() {
diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index bb6fbcd899..0c61b0d01a 100644
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -73,6 +73,7 @@
 #include "MovingEntitiesOperator.h"
 #include "SceneScriptingInterface.h"
 #include "WarningsSuppression.h"
+#include "ScriptPermissions.h"
 
 using namespace std;
 
@@ -226,7 +227,7 @@ MyAvatar::MyAvatar(QThread* thread) :
     _yawSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "yawSpeed", _yawSpeed),
     _hmdYawSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "hmdYawSpeed", _hmdYawSpeed),
     _pitchSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "pitchSpeed", _pitchSpeed),
-    _fullAvatarURLSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "fullAvatarURL",
+    _fullAvatarURLSetting(QStringList() << SETTINGS_FULL_PRIVATE_GROUP_NAME << AVATAR_SETTINGS_GROUP_NAME << "fullAvatarURL",
                           AvatarData::defaultFullAvatarModelUrl()),
     _fullAvatarModelNameSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "fullAvatarModelName", _fullAvatarModelName),
     _animGraphURLSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "animGraphURL", QUrl("")),
@@ -1035,8 +1036,8 @@ void MyAvatar::simulate(float deltaTime, bool inView) {
         std::pair<bool, bool> zoneInteractionProperties;
         entityTree->withWriteLock([&] {
             zoneInteractionProperties = entityTreeRenderer->getZoneInteractionProperties();
-            EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
-            entityTree->updateEntityQueryAACube(shared_from_this(), packetSender, false, true);
+            std::shared_ptr<EntityEditPacketSender> packetSender = qApp->getEntityEditPacketSender();
+            entityTree->updateEntityQueryAACube(shared_from_this(), packetSender.get(), false, true);
         });
         bool isPhysicsEnabled = qApp->isPhysicsEnabled();
         bool zoneAllowsFlying = zoneInteractionProperties.first;
@@ -1729,7 +1730,7 @@ void MyAvatar::handleChangedAvatarEntityData() {
     entityTree->deleteEntitiesByID(entitiesToDelete);
 
     // ADD real entities
-    EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
+    auto packetSender = qApp->getEntityEditPacketSender();
     for (const auto& id : entitiesToAdd) {
         bool blobFailed = false;
         EntityItemProperties properties;
@@ -1739,10 +1740,11 @@ void MyAvatar::handleChangedAvatarEntityData() {
                 blobFailed = true; // blob doesn't exist
                 return;
             }
-            std::lock_guard<std::mutex> guard(_scriptEngineLock);
-            if (!EntityItemProperties::blobToProperties(*_scriptEngine, itr.value(), properties)) {
-                blobFailed = true; // blob is corrupt
-            }
+            _helperScriptEngine.run( [&] {
+                if (!EntityItemProperties::blobToProperties(*_helperScriptEngine.get(), itr.value(), properties)) {
+                    blobFailed = true;  // blob is corrupt
+                }
+            });
         });
         if (blobFailed) {
             // remove from _cachedAvatarEntityBlobUpdatesToSkip just in case:
@@ -1775,10 +1777,11 @@ void MyAvatar::handleChangedAvatarEntityData() {
                 skip = true;
                 return;
             }
-            std::lock_guard<std::mutex> guard(_scriptEngineLock);
-            if (!EntityItemProperties::blobToProperties(*_scriptEngine, itr.value(), properties)) {
-                skip = true;
-            }
+            _helperScriptEngine.run( [&] {
+                if (!EntityItemProperties::blobToProperties(*_helperScriptEngine.get(), itr.value(), properties)) {
+                    skip = true;
+                }
+            });
         });
         if (!skip && canRezAvatarEntites) {
             sanitizeAvatarEntityProperties(properties);
@@ -1883,10 +1886,9 @@ bool MyAvatar::updateStaleAvatarEntityBlobs() const {
         if (found) {
             ++numFound;
             QByteArray blob;
-            {
-                std::lock_guard<std::mutex> guard(_scriptEngineLock);
-                EntityItemProperties::propertiesToBlob(*_scriptEngine, getID(), properties, blob);
-            }
+            _helperScriptEngine.run( [&] {
+                EntityItemProperties::propertiesToBlob(*_helperScriptEngine.get(), getID(), properties, blob);
+            });
             _avatarEntitiesLock.withWriteLock([&] {
                 _cachedAvatarEntityBlobs[id] = blob;
             });
@@ -1947,10 +1949,9 @@ AvatarEntityMap MyAvatar::getAvatarEntityData() const {
         EntityItemProperties properties = entity->getProperties(desiredProperties);
 
         QByteArray blob;
-        {
-            std::lock_guard<std::mutex> guard(_scriptEngineLock);
-            EntityItemProperties::propertiesToBlob(*_scriptEngine, getID(), properties, blob, true);
-        }
+        _helperScriptEngine.run( [&] {
+            EntityItemProperties::propertiesToBlob(*_helperScriptEngine.get(), getID(), properties, blob, true);
+        });
 
         data[entityID] = blob;
     }
@@ -2092,9 +2093,6 @@ void MyAvatar::avatarEntityDataToJson(QJsonObject& root) const {
 }
 
 void MyAvatar::loadData() {
-    if (!_scriptEngine) {
-        _scriptEngine = newScriptEngine();
-    }
     getHead()->setBasePitch(_headPitchSetting.get());
 
     _yawSpeed = _yawSpeedSetting.get(_yawSpeed);
@@ -2236,6 +2234,9 @@ AttachmentData MyAvatar::loadAttachmentData(const QUrl& modelURL, const QString&
     return attachment;
 }
 
+bool MyAvatar::isMyAvatarURLProtected() const {
+    return !ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL);
+}
 
 int MyAvatar::parseDataFromBuffer(const QByteArray& buffer) {
     qCDebug(interfaceapp) << "Error: ignoring update packet for MyAvatar"
@@ -2700,11 +2701,10 @@ QVariantList MyAvatar::getAvatarEntitiesVariant() {
             QVariantMap avatarEntityData;
             avatarEntityData["id"] = entityID;
             EntityItemProperties entityProperties = entity->getProperties(desiredProperties);
-            {
-                std::lock_guard<std::mutex> guard(_scriptEngineLock);
-                ScriptValue scriptProperties = EntityItemPropertiesToScriptValue(_scriptEngine.get(), entityProperties);
+            _helperScriptEngine.run( [&] {
+                ScriptValue scriptProperties = EntityItemPropertiesToScriptValue(_helperScriptEngine.get(), entityProperties);
                 avatarEntityData["properties"] = scriptProperties.toVariant();
-            }
+            });
             avatarEntitiesData.append(QVariant(avatarEntityData));
         }
     }
@@ -4232,7 +4232,7 @@ void MyAvatar::setSessionUUID(const QUuid& sessionUUID) {
             _avatarEntitiesLock.withReadLock([&] {
                 avatarEntityIDs = _packedAvatarEntityData.keys();
             });
-            EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
+            auto packetSender = qApp->getEntityEditPacketSender();
             entityTree->withWriteLock([&] {
                 for (const auto& entityID : avatarEntityIDs) {
                     auto entity = entityTree->findEntityByID(entityID);
@@ -5769,8 +5769,7 @@ void MyAvatar::FollowHelper::deactivate() {
 }
 
 void MyAvatar::FollowHelper::deactivate(CharacterController::FollowType type) {
-    int int_type = static_cast<int>(type);
-    assert(int_type >= 0 && int_type < static_cast<int>(CharacterController::FollowType::Count));
+    assert(type < CharacterController::FollowType::Count);
     _timeRemaining[(int)type] = 0.0f;
 }
 
@@ -5778,16 +5777,14 @@ void MyAvatar::FollowHelper::deactivate(CharacterController::FollowType type) {
 // eg. activate(FollowType::Rotation, true) snaps the FollowHelper's rotation immediately
 // to the rotation of its _followDesiredBodyTransform.
 void MyAvatar::FollowHelper::activate(CharacterController::FollowType type, const bool snapFollow) {
-    int int_type = static_cast<int>(type);
-    assert(int_type >= 0 && int_type < static_cast<int>(CharacterController::FollowType::Count));
+    assert(type < CharacterController::FollowType::Count);
 
     // TODO: Perhaps, the follow time should be proportional to the displacement.
     _timeRemaining[(int)type] = snapFollow ? CharacterController::FOLLOW_TIME_IMMEDIATE_SNAP : FOLLOW_TIME;
 }
 
 bool MyAvatar::FollowHelper::isActive(CharacterController::FollowType type) const {
-    int int_type = static_cast<int>(type);
-    assert(int_type >= 0 && int_type < static_cast<int>(CharacterController::FollowType::Count));
+    assert(type < CharacterController::FollowType::Count);
     return _timeRemaining[(int)type] > 0.0f;
 }
 
@@ -6889,7 +6886,7 @@ void MyAvatar::sendPacket(const QUuid& entityID) const {
     if (entityTree) {
         entityTree->withWriteLock([&] {
             // force an update packet
-            EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
+            auto packetSender = qApp->getEntityEditPacketSender();
             packetSender->queueEditAvatarEntityMessage(entityTree, entityID);
         });
     }
diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h
index 5e0627360c..60c07ad42c 100644
--- a/interface/src/avatar/MyAvatar.h
+++ b/interface/src/avatar/MyAvatar.h
@@ -28,6 +28,7 @@
 #include <controllers/Pose.h>
 #include <controllers/Actions.h>
 #include <EntityItem.h>
+#include <HelperScriptEngine.h>
 #include <ThreadSafeValueCache.h>
 #include <Rig.h>
 #include <SettingHandle.h>
@@ -2683,6 +2684,7 @@ private:
     void setEnableDrawAverageFacing(bool drawAverage) { _drawAverageFacingEnabled = drawAverage; }
     bool getEnableDrawAverageFacing() const { return _drawAverageFacingEnabled; }
     virtual bool isMyAvatar() const override { return true; }
+    virtual bool isMyAvatarURLProtected() const override;
     virtual int parseDataFromBuffer(const QByteArray& buffer) override;
     virtual glm::vec3 getSkeletonPosition() const override;
     int _skeletonModelChangeCount { 0 };
@@ -3101,8 +3103,10 @@ private:
     mutable std::set<EntityItemID> _staleCachedAvatarEntityBlobs;
     //
     // keep a ScriptEngine around so we don't have to instantiate on the fly (these are very slow to create/delete)
-    mutable std::mutex _scriptEngineLock;
-    ScriptEnginePointer _scriptEngine { nullptr };
+    // TODO: profile if it performs better when script engine is on avatar thread or on its own thread
+    // Own thread is safer from deadlocks
+    mutable HelperScriptEngine _helperScriptEngine;
+
     bool _needToSaveAvatarEntitySettings { false };
 
     bool _reactionTriggers[NUM_AVATAR_TRIGGER_REACTIONS] { false, false };
diff --git a/interface/src/graphics/GraphicsEngine.cpp b/interface/src/graphics/GraphicsEngine.cpp
index bf69efd23e..5075d9b57f 100644
--- a/interface/src/graphics/GraphicsEngine.cpp
+++ b/interface/src/graphics/GraphicsEngine.cpp
@@ -281,7 +281,7 @@ void GraphicsEngine::render_performFrame() {
 
         {
             PROFILE_RANGE(render, "/runRenderFrame");
-            renderArgs._hudOperator = displayPlugin->getHUDOperator();
+            renderArgs._hudOperator = qApp->getApplicationOverlay().enabled() ? displayPlugin->getHUDOperator() : nullptr;
             renderArgs._hudTexture = qApp->getApplicationOverlay().getOverlayTexture();
             renderArgs._takingSnapshot = qApp->takeSnapshotOperators(snapshotOperators);
             renderArgs._blitFramebuffer = finalFramebuffer;
diff --git a/interface/src/main.cpp b/interface/src/main.cpp
index 835e4060a7..7e8c1afff3 100644
--- a/interface/src/main.cpp
+++ b/interface/src/main.cpp
@@ -24,6 +24,7 @@
 #include <SharedUtil.h>
 #include <NetworkAccessManager.h>
 #include <gl/GLHelpers.h>
+#include <iostream>
 
 #include "AddressManager.h"
 #include "Application.h"
@@ -33,6 +34,9 @@
 #include "MainWindow.h"
 #include "Profile.h"
 #include "LogHandler.h"
+#include <plugins/PluginManager.h>
+#include <plugins/DisplayPlugin.h>
+#include <plugins/CodecPlugin.h>
 
 #ifdef Q_OS_WIN
 #include <Windows.h>
@@ -63,11 +67,24 @@ int main(int argc, const char* argv[]) {
     }
 #endif
 
+    // Setup QCoreApplication settings, install log message handler
     setupHifiApplication(BuildInfo::INTERFACE_NAME);
 
     // Journald by default in user applications is probably a bit too modern still.
     LogHandler::getInstance().setShouldUseJournald(false);
 
+
+    // Extend argv to enable WebGL rendering
+    std::vector<const char*> argvExtended(&argv[0], &argv[argc]);
+    argvExtended.push_back("--ignore-gpu-blocklist");
+#ifdef Q_OS_ANDROID
+    argvExtended.push_back("--suppress-settings-reset");
+#endif
+    int argcExtended = (int)argvExtended.size();
+
+    QElapsedTimer startupTime;
+    startupTime.start();
+
     QCommandLineParser parser;
     parser.setApplicationDescription("Overte -- A free/libre and open-source virtual worlds client");
     QCommandLineOption helpOption = parser.addHelpOption();
@@ -125,12 +142,12 @@ int main(int argc, const char* argv[]) {
         "displays"
     );
     QCommandLineOption disableDisplaysOption(
-        "disable-displays",
+        "disableDisplayPlugins",
         "Displays to disable. Valid options include \"OpenVR (Vive)\" and \"Oculus Rift\"",
         "string"
     );
     QCommandLineOption disableInputsOption(
-        "disable-inputs",
+        "disableInputPlugins",
         "Inputs to disable. Valid options include \"OpenVR (Vive)\" and \"Oculus Rift\"",
         "string"
     );
@@ -246,6 +263,19 @@ int main(int argc, const char* argv[]) {
         "Logging options, comma separated: color,nocolor,process_id,thread_id,milliseconds,keep_repeats,journald,nojournald",
         "options"
     );
+    QCommandLineOption getPluginsOption(
+        "getPlugins",
+        "Print out a list of plugins in JSON"
+    );
+    QCommandLineOption abortAfterStartupOption(
+        "abortAfterStartup",
+        "Debug option. Aborts right after startup."
+    );
+    QCommandLineOption abortAfterInitOption(
+        "abortAfterInit",
+        "Debug option. Aborts after initialization, right before the program starts running the event loop."
+    );
+
     // "--qmljsdebugger", which appears in output from "--help-all".
     // Those below don't seem to be optional.
     //     --ignore-gpu-blacklist
@@ -288,6 +318,10 @@ int main(int argc, const char* argv[]) {
     parser.addOption(quitWhenFinishedOption);
     parser.addOption(fastHeartbeatOption);
     parser.addOption(logOption);
+    parser.addOption(abortAfterStartupOption);
+    parser.addOption(abortAfterInitOption);
+    parser.addOption(getPluginsOption);
+
 
     QString applicationPath;
     // A temporary application instance is needed to get the location of the running executable
@@ -310,6 +344,16 @@ int main(int argc, const char* argv[]) {
 #endif
     }
 
+    // TODO: We need settings for Application, but Settings needs an Application
+    // to handle events. Needs splitting into two parts: enough initialization
+    // for Application to work, and then thread start afterwards.
+    Setting::init();
+    Application app(argcExtended, const_cast<char**>(argvExtended.data()), parser, startupTime);
+
+    if (parser.isSet("abortAfterStartup")) {
+        return 99;
+    }
+
     // We want to configure the logging system as early as possible
     auto& logHandler = LogHandler::getInstance();
 
@@ -321,6 +365,75 @@ int main(int argc, const char* argv[]) {
         }
     }
 
+    app.initializePluginManager(parser);
+
+    if (parser.isSet(getPluginsOption)) {
+        auto pluginManager = PluginManager::getInstance();
+
+        QJsonObject pluginsJson;
+        for (const auto &plugin : pluginManager->getPluginInfo()) {
+            QJsonObject data;
+            data["data"] = plugin.metaData;
+            data["loaded"] = plugin.loaded;
+            data["disabled"] = plugin.disabled;
+            data["filteredOut"] = plugin.filteredOut;
+            data["wrongVersion"] = plugin.wrongVersion;
+            pluginsJson[plugin.name] = data;
+        }
+
+        QJsonObject inputJson;
+        for (const auto &plugin : pluginManager->getInputPlugins()) {
+            QJsonObject data;
+            data["subdeviceNames"] = QJsonArray::fromStringList(plugin->getSubdeviceNames());
+            data["deviceName"] = plugin->getDeviceName();
+            data["configurable"] = plugin->configurable();
+            data["isHandController"] = plugin->isHandController();
+            data["isHeadController"] = plugin->isHeadController();
+            data["isActive"] = plugin->isActive();
+            data["isSupported"] = plugin->isSupported();
+
+            inputJson[plugin->getName()] = data;
+        }
+
+        QJsonObject displayJson;
+        for (const auto &plugin : pluginManager->getDisplayPlugins()) {
+            QJsonObject data;
+            data["isHmd"] = plugin->isHmd();
+            data["isStereo"] = plugin->isStereo();
+            data["targetFramerate"] = plugin->getTargetFrameRate();
+            data["hasAsyncReprojection"] = plugin->hasAsyncReprojection();
+            data["isActive"] = plugin->isActive();
+            data["isSupported"] = plugin->isSupported();
+
+            displayJson[plugin->getName()] = data;
+        }
+
+        QJsonObject codecsJson;
+        for (const auto &plugin : pluginManager->getCodecPlugins()) {
+            QJsonObject data;
+            data["isActive"] = plugin->isActive();
+            data["isSupported"] = plugin->isSupported();
+
+            codecsJson[plugin->getName()] = data;
+        }
+
+        QJsonObject platformsJson;
+        platformsJson["steamAvailable"] = (pluginManager->getSteamClientPlugin() != nullptr);
+        platformsJson["oculusAvailable"] = (pluginManager->getOculusPlatformPlugin() != nullptr);
+
+        QJsonObject root;
+        root["plugins"] = pluginsJson;
+        root["inputs"] = inputJson;
+        root["displays"] = displayJson;
+        root["codecs"] = codecsJson;
+        root["platforms"] = platformsJson;
+
+        std::cout << QJsonDocument(root).toJson().toStdString() << "\n";
+
+        return 0;
+    }
+
+
     // Act on arguments for early termination.
     if (parser.isSet(versionOption)) {
         parser.showVersion();
@@ -407,10 +520,9 @@ int main(int argc, const char* argv[]) {
     QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
 #endif
 
-    QElapsedTimer startupTime;
-    startupTime.start();
 
-    Setting::init();
+
+
 
     // Instance UserActivityLogger now that the settings are loaded
     auto& ual = UserActivityLogger::getInstance();
@@ -549,7 +661,7 @@ int main(int argc, const char* argv[]) {
     // Oculus initialization MUST PRECEDE OpenGL context creation.
     // The nature of the Application constructor means this has to be either here,
     // or in the main window ctor, before GL startup.
-    Application::initPlugins(parser);
+    //app.configurePlugins(parser);
 
 #ifdef Q_OS_WIN
     // If we're running in steam mode, we need to do an explicit check to ensure we're up to the required min spec
@@ -587,17 +699,10 @@ int main(int argc, const char* argv[]) {
             SandboxUtils::runLocalSandbox(serverContentPath, true, noUpdater);
         }
 
-        // Extend argv to enable WebGL rendering
-        std::vector<const char*> argvExtended(&argv[0], &argv[argc]);
-        argvExtended.push_back("--ignore-gpu-blocklist");
-#ifdef Q_OS_ANDROID
-        argvExtended.push_back("--suppress-settings-reset");
-#endif
-        int argcExtended = (int)argvExtended.size();
-
         PROFILE_SYNC_END(startup, "main startup", "");
         PROFILE_SYNC_BEGIN(startup, "app full ctor", "");
-        Application app(argcExtended, const_cast<char**>(argvExtended.data()), parser, startupTime, runningMarkerExisted);
+        app.setPreviousSessionCrashed(runningMarkerExisted);
+        app.initialize(parser);
         PROFILE_SYNC_END(startup, "app full ctor", "");
 
 #if defined(Q_OS_LINUX)
@@ -665,6 +770,9 @@ int main(int argc, const char* argv[]) {
         translator.load("i18n/interface_en");
         app.installTranslator(&translator);
         qCDebug(interfaceapp, "Created QT Application.");
+        if (parser.isSet("abortAfterInit")) {
+            return 99;
+        }
         exitCode = app.exec();
         server.close();
 
diff --git a/interface/src/raypick/LaserPointer.cpp b/interface/src/raypick/LaserPointer.cpp
index ce44d3011c..91af721ea6 100644
--- a/interface/src/raypick/LaserPointer.cpp
+++ b/interface/src/raypick/LaserPointer.cpp
@@ -137,12 +137,12 @@ LaserPointer::RenderState::RenderState(const QUuid& startID, const QUuid& pathID
         {
             EntityPropertyFlags desiredProperties;
             desiredProperties += PROP_IGNORE_PICK_INTERSECTION;
-            _pathIgnorePicks = entityScriptingInterface->getEntityPropertiesInternal(getPathID(), desiredProperties).getIgnorePickIntersection();
+            _pathIgnorePicks = entityScriptingInterface->getEntityPropertiesInternal(getPathID(), desiredProperties, false).getIgnorePickIntersection();
         }
         {
             EntityPropertyFlags desiredProperties;
             desiredProperties += PROP_STROKE_WIDTHS;
-            auto widths = entityScriptingInterface->getEntityPropertiesInternal(getPathID(), desiredProperties).getStrokeWidths();
+            auto widths = entityScriptingInterface->getEntityPropertiesInternal(getPathID(), desiredProperties, false).getStrokeWidths();
             _lineWidth = widths.length() == 0 ? PolyLineEntityItem::DEFAULT_LINE_WIDTH : widths[0];
         }
     }
diff --git a/interface/src/raypick/ParabolaPick.cpp b/interface/src/raypick/ParabolaPick.cpp
index 378a46b96b..e8dd759449 100644
--- a/interface/src/raypick/ParabolaPick.cpp
+++ b/interface/src/raypick/ParabolaPick.cpp
@@ -71,7 +71,7 @@ PickResultPointer ParabolaPick::getEntityIntersection(const PickParabola& pick)
             if (getFilter().doesPickLocalEntities()) {
                 EntityPropertyFlags desiredProperties;
                 desiredProperties += PROP_ENTITY_HOST_TYPE;
-                if (DependencyManager::get<EntityScriptingInterface>()->getEntityPropertiesInternal(entityRes.entityID, desiredProperties).getEntityHostType() == entity::HostType::LOCAL) {
+                if (DependencyManager::get<EntityScriptingInterface>()->getEntityPropertiesInternal(entityRes.entityID, desiredProperties, false).getEntityHostType() == entity::HostType::LOCAL) {
                     type = IntersectionType::LOCAL_ENTITY;
                 }
             }
diff --git a/interface/src/raypick/PathPointer.cpp b/interface/src/raypick/PathPointer.cpp
index b24c5630c4..9333e0f03b 100644
--- a/interface/src/raypick/PathPointer.cpp
+++ b/interface/src/raypick/PathPointer.cpp
@@ -257,7 +257,7 @@ StartEndRenderState::StartEndRenderState(const QUuid& startID, const QUuid& endI
         EntityPropertyFlags desiredProperties;
         desiredProperties += PROP_DIMENSIONS;
         desiredProperties += PROP_IGNORE_PICK_INTERSECTION;
-        auto properties = entityScriptingInterface->getEntityPropertiesInternal(_startID, desiredProperties);
+        auto properties = entityScriptingInterface->getEntityPropertiesInternal(_startID, desiredProperties, false);
         _startDim = properties.getDimensions();
         _startIgnorePicks = properties.getIgnorePickIntersection();
     }
@@ -266,7 +266,7 @@ StartEndRenderState::StartEndRenderState(const QUuid& startID, const QUuid& endI
         desiredProperties += PROP_DIMENSIONS;
         desiredProperties += PROP_ROTATION;
         desiredProperties += PROP_IGNORE_PICK_INTERSECTION;
-        auto properties = entityScriptingInterface->getEntityPropertiesInternal(_endID, desiredProperties);
+        auto properties = entityScriptingInterface->getEntityPropertiesInternal(_endID, desiredProperties, false);
         _endDim = properties.getDimensions();
         _endRot = properties.getRotation();
         _endIgnorePicks = properties.getIgnorePickIntersection();
diff --git a/interface/src/raypick/RayPick.cpp b/interface/src/raypick/RayPick.cpp
index 17326baa1a..4cb7232095 100644
--- a/interface/src/raypick/RayPick.cpp
+++ b/interface/src/raypick/RayPick.cpp
@@ -40,7 +40,7 @@ PickResultPointer RayPick::getEntityIntersection(const PickRay& pick) {
         if (getFilter().doesPickLocalEntities()) {
             EntityPropertyFlags desiredProperties;
             desiredProperties += PROP_ENTITY_HOST_TYPE;
-            if (DependencyManager::get<EntityScriptingInterface>()->getEntityPropertiesInternal(entityRes.entityID, desiredProperties).getEntityHostType() == entity::HostType::LOCAL) {
+            if (DependencyManager::get<EntityScriptingInterface>()->getEntityPropertiesInternal(entityRes.entityID, desiredProperties, false).getEntityHostType() == entity::HostType::LOCAL) {
                 type = IntersectionType::LOCAL_ENTITY;
             }
         }
@@ -123,6 +123,6 @@ glm::vec2 RayPick::projectOntoEntityXYPlane(const QUuid& entityID, const glm::ve
     desiredProperties += PROP_ROTATION;
     desiredProperties += PROP_DIMENSIONS;
     desiredProperties += PROP_REGISTRATION_POINT;
-    auto props = DependencyManager::get<EntityScriptingInterface>()->getEntityPropertiesInternal(entityID, desiredProperties);
+    auto props = DependencyManager::get<EntityScriptingInterface>()->getEntityPropertiesInternal(entityID, desiredProperties, false);
     return projectOntoXYPlane(worldPos, props.getPosition(), props.getRotation(), props.getDimensions(), props.getRegistrationPoint(), unNormalized);
 }
diff --git a/interface/src/scripting/ControllerScriptingInterface.h b/interface/src/scripting/ControllerScriptingInterface.h
index 175021e559..4128976fdc 100644
--- a/interface/src/scripting/ControllerScriptingInterface.h
+++ b/interface/src/scripting/ControllerScriptingInterface.h
@@ -216,6 +216,7 @@ class ScriptEngine;
  *
  * @property {Controller.Actions} Actions - Predefined actions on Interface and the user's avatar. These can be used as end
  *     points in a {@link RouteObject} mapping. A synonym for <code>Controller.Hardware.Actions</code>.
+ *     Getting this property is computationally expensive, so it's best to cache it once on script start.
  *     <em>Read-only.</em>
  *     <p>Default mappings are provided from the <code>Controller.Hardware.Keyboard</code> and <code>Controller.Standard</code> 
  *     to actions in 
@@ -225,13 +226,16 @@ class ScriptEngine;
  *     standard.json</a>, respectively.</p>
  *
  * @property {Controller.Hardware} Hardware - Standard and hardware-specific controller and computer outputs, plus predefined 
- *     actions on Interface and the user's avatar. The outputs can be mapped to <code>Actions</code> or functions in a 
+ *     actions on Interface and the user's avatar. Getting this property is computationally expensive, so it's best to cache it
+ *     instead of calling on every update.
+ *     The outputs can be mapped to <code>Actions</code> or functions in a
  *     {@link RouteObject} mapping. Additionally, hardware-specific controller outputs can be mapped to 
  *     <code>Controller.Standard</code> controller outputs. <em>Read-only.</em>
  *
  * @property {Controller.Standard} Standard - Standard controller outputs that can be mapped to <code>Actions</code> or 
  *     functions in a {@link RouteObject} mapping. <em>Read-only.</em>
- *     <p>Each hardware device has a mapping from its outputs to <code>Controller.Standard</code> items, specified in a JSON file. 
+ *     <p>Each hardware device has a mapping from its outputs to <code>Controller.Standard</code> items, specified in a JSON file.
+ *     Getting this property is computationally expensive, so it's best to cache it once on script start.
  *     For example, <a href="https://github.com/highfidelity/hifi/blob/master/interface/resources/controllers/leapmotion.json">
  *     leapmotion.json</a> and 
  *     <a href="https://github.com/highfidelity/hifi/blob/master/interface/resources/controllers/vive.json">vive.json</a>.</p>
diff --git a/interface/src/scripting/PerformanceScriptingInterface.cpp b/interface/src/scripting/PerformanceScriptingInterface.cpp
index 9f3534b3e8..f619729eff 100644
--- a/interface/src/scripting/PerformanceScriptingInterface.cpp
+++ b/interface/src/scripting/PerformanceScriptingInterface.cpp
@@ -56,12 +56,22 @@ void PerformanceScriptingInterface::setRefreshRateProfile(RefreshRateProfile ref
     emit settingsChanged();
 }
 
+void PerformanceScriptingInterface::setCustomRefreshRate(RefreshRateManager::RefreshRateRegime refreshRateRegime, int value)
+{
+    qApp->getRefreshRateManager().setCustomRefreshRate(refreshRateRegime, value);
+    emit settingsChanged();
+}
+
+int PerformanceScriptingInterface::getCustomRefreshRate(RefreshRateManager::RefreshRateRegime refreshRateRegime) const {
+    return qApp->getRefreshRateManager().getCustomRefreshRate(refreshRateRegime);
+}
+
 PerformanceScriptingInterface::RefreshRateProfile PerformanceScriptingInterface::getRefreshRateProfile() const {
     return (PerformanceScriptingInterface::RefreshRateProfile)qApp->getRefreshRateManager().getRefreshRateProfile();
 }
 
 QStringList PerformanceScriptingInterface::getRefreshRateProfileNames() const {
-    static const QStringList refreshRateProfileNames = { "ECO", "INTERACTIVE", "REALTIME" };
+    static const QStringList refreshRateProfileNames = { "ECO", "INTERACTIVE", "REALTIME", "CUSTOM" };
     return refreshRateProfileNames;
 }
 
diff --git a/interface/src/scripting/PerformanceScriptingInterface.h b/interface/src/scripting/PerformanceScriptingInterface.h
index 86350c8a1f..05e2c1d7bc 100644
--- a/interface/src/scripting/PerformanceScriptingInterface.h
+++ b/interface/src/scripting/PerformanceScriptingInterface.h
@@ -127,6 +127,22 @@ public slots:
      */
     void setRefreshRateProfile(RefreshRateProfile refreshRateProfile);
 
+    /*@jsdoc
+     * Sets a custom refresh rate.
+     * @function Performance.setCustomRefreshRate
+     * @param {RefreshRateRegime} refreshRateRegime - The refresh rate regime
+     * @param {int} value - The value for the regime
+     */
+    void setCustomRefreshRate(RefreshRateManager::RefreshRateRegime refreshRateRegime, int value);
+
+    /*@jsdoc
+     * Gets the value for a specific RefreshRateRegime.
+     * @function Performance.getCustomRefreshRate
+     * @param {RefreshRateRegime} - The regime to get the value from
+     * @returns {int} - The value from the specified regime
+     */
+    int getCustomRefreshRate(RefreshRateManager::RefreshRateRegime regime) const;
+
     /*@jsdoc
      * Gets the current refresh rate profile in use.
      * @function Performance.getRefreshRateProfile
diff --git a/interface/src/scripting/SettingsScriptingInterface.cpp b/interface/src/scripting/SettingsScriptingInterface.cpp
index b7ef172f19..00cdf009eb 100644
--- a/interface/src/scripting/SettingsScriptingInterface.cpp
+++ b/interface/src/scripting/SettingsScriptingInterface.cpp
@@ -21,6 +21,9 @@ SettingsScriptingInterface* SettingsScriptingInterface::getInstance() {
 }
 
 QVariant SettingsScriptingInterface::getValue(const QString& setting) {
+    if (_restrictPrivateValues && setting.startsWith(SETTINGS_FULL_PRIVATE_GROUP_NAME + "/")) {
+        return {""};
+    }
     QVariant value = Setting::Handle<QVariant>(setting).get();
     if (!value.isValid()) {
         value = "";
@@ -29,6 +32,9 @@ QVariant SettingsScriptingInterface::getValue(const QString& setting) {
 }
 
 QVariant SettingsScriptingInterface::getValue(const QString& setting, const QVariant& defaultValue) {
+    if (_restrictPrivateValues && setting.startsWith(SETTINGS_FULL_PRIVATE_GROUP_NAME + "/")) {
+        return {""};
+    }
     QVariant value = Setting::Handle<QVariant>(setting, defaultValue).get();
     if (!value.isValid()) {
         value = "";
@@ -40,7 +46,7 @@ void SettingsScriptingInterface::setValue(const QString& setting, const QVariant
     if (getValue(setting) == value) {
         return;
     }
-    if (setting.startsWith("private/")) {
+    if (setting.startsWith("private/") || setting.startsWith(SETTINGS_FULL_PRIVATE_GROUP_NAME + "/")) {
         if (_restrictPrivateValues) {
             qWarning() << "SettingsScriptingInterface::setValue -- restricted write: " << setting << value;
             return;
diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp
index 27564b39eb..b70798d02a 100644
--- a/interface/src/scripting/WindowScriptingInterface.cpp
+++ b/interface/src/scripting/WindowScriptingInterface.cpp
@@ -78,7 +78,7 @@ WindowScriptingInterface::~WindowScriptingInterface() {
 }
 
 ScriptValue WindowScriptingInterface::hasFocus() {
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     return engine()->newValue(qApp->hasFocus());
 }
 
@@ -107,7 +107,7 @@ void WindowScriptingInterface::alert(const QString& message) {
 /// \param const QString& message message to display
 /// \return ScriptValue `true` if 'Yes' was clicked, `false` otherwise
 ScriptValue WindowScriptingInterface::confirm(const QString& message) {
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     return engine()->newValue((QMessageBox::Yes == OffscreenUi::question("", message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)));
 }
 
@@ -117,7 +117,7 @@ ScriptValue WindowScriptingInterface::confirm(const QString& message) {
 /// \return ScriptValue string text value in text box if the dialog was accepted, `null` otherwise.
 ScriptValue WindowScriptingInterface::prompt(const QString& message, const QString& defaultText) {
     QString result = OffscreenUi::getText(nullptr, "", message, QLineEdit::Normal, defaultText);
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     auto sResult = engine()->newValue(result);
     if (sResult.equals(engine()->newValue(""))) {
         return engine()->nullValue();
@@ -234,7 +234,7 @@ ScriptValue WindowScriptingInterface::browseDir(const QString& title, const QStr
     if (!result.isEmpty()) {
         setPreviousBrowseLocation(QFileInfo(result).absolutePath());
     }
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     return result.isEmpty() ? engine()->nullValue() : engine()->newValue(result);
 }
 
@@ -279,7 +279,7 @@ ScriptValue WindowScriptingInterface::browse(const QString& title, const QString
     if (!result.isEmpty()) {
         setPreviousBrowseLocation(QFileInfo(result).absolutePath());
     }
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     return result.isEmpty() ? engine()->nullValue() : engine()->newValue(result);
 }
 
@@ -327,7 +327,7 @@ ScriptValue WindowScriptingInterface::save(const QString& title, const QString&
     if (!result.isEmpty()) {
         setPreviousBrowseLocation(QFileInfo(result).absolutePath());
     }
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     return result.isEmpty() ? engine()->nullValue() : engine()->newValue(result);
 }
 
@@ -378,7 +378,7 @@ ScriptValue WindowScriptingInterface::browseAssets(const QString& title, const Q
     if (!result.isEmpty()) {
         setPreviousBrowseAssetLocation(QFileInfo(result).absolutePath());
     }
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     return result.isEmpty() ? engine()->nullValue() : engine()->newValue(result);
 }
 
diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h
index 23055cf246..9db09b0b9b 100644
--- a/interface/src/ui/ApplicationOverlay.h
+++ b/interface/src/ui/ApplicationOverlay.h
@@ -27,18 +27,17 @@ public:
 
     void renderOverlay(RenderArgs* renderArgs);
 
-    gpu::TexturePointer getOverlayTexture(); 
+    gpu::TexturePointer getOverlayTexture();
+
+    bool enabled() const { return _enabled; }
+    void setEnabled(bool enabled) { _enabled = enabled; }
 
 private:
-    void renderStatsAndLogs(RenderArgs* renderArgs);
     void renderDomainConnectionStatusBorder(RenderArgs* renderArgs);
     void renderQmlUi(RenderArgs* renderArgs);
     void renderOverlays(RenderArgs* renderArgs);
     void buildFramebufferObject();
 
-    float _alpha{ 1.0f };
-    float _trailingAudioLoudness{ 0.0f };
-
     int _domainStatusBorder;
     int _magnifierBorder;
 
@@ -47,6 +46,8 @@ private:
     gpu::TexturePointer _overlayColorTexture;
     gpu::FramebufferPointer _overlayFramebuffer;
     int _qmlGeometryId { 0 };
+
+    bool _enabled { true };
 };
 
 #endif // hifi_ApplicationOverlay_h
diff --git a/interface/src/ui/Keyboard.cpp b/interface/src/ui/Keyboard.cpp
index 80ba33d0c7..d8038e8727 100644
--- a/interface/src/ui/Keyboard.cpp
+++ b/interface/src/ui/Keyboard.cpp
@@ -118,7 +118,7 @@ std::pair<glm::vec3, glm::quat> calculateKeyboardPositionAndOrientation() {
         EntityPropertyFlags desiredProperties;
         desiredProperties += PROP_POSITION;
         desiredProperties += PROP_ROTATION;
-        auto properties = DependencyManager::get<EntityScriptingInterface>()->getEntityPropertiesInternal(tabletID, desiredProperties);
+        auto properties = DependencyManager::get<EntityScriptingInterface>()->getEntityPropertiesInternal(tabletID, desiredProperties, false);
 
         auto tablet = DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system");
         bool landscapeMode = tablet->getLandscape();
@@ -146,7 +146,7 @@ void Key::saveDimensionsAndLocalPosition() {
     EntityPropertyFlags desiredProperties;
     desiredProperties += PROP_LOCAL_POSITION;
     desiredProperties += PROP_DIMENSIONS;
-    auto properties = DependencyManager::get<EntityScriptingInterface>()->getEntityPropertiesInternal(_keyID, desiredProperties);
+    auto properties = DependencyManager::get<EntityScriptingInterface>()->getEntityPropertiesInternal(_keyID, desiredProperties, false);
 
     _originalLocalPosition = properties.getLocalPosition();
     _originalDimensions = properties.getDimensions();
@@ -469,7 +469,7 @@ void Keyboard::switchToLayer(int layerIndex) {
         EntityPropertyFlags desiredProperties;
         desiredProperties += PROP_POSITION;
         desiredProperties += PROP_ROTATION;
-        auto oldProperties = entityScriptingInterface->getEntityPropertiesInternal(_anchor.entityID, desiredProperties);
+        auto oldProperties = entityScriptingInterface->getEntityPropertiesInternal(_anchor.entityID, desiredProperties, false);
 
         glm::vec3 currentPosition = oldProperties.getPosition();
         glm::quat currentOrientation = oldProperties.getRotation();
@@ -530,7 +530,7 @@ void Keyboard::handleTriggerBegin(const QUuid& id, const PointerEvent& event) {
 
         EntityPropertyFlags desiredProperties;
         desiredProperties += PROP_POSITION;
-        glm::vec3 keyWorldPosition = DependencyManager::get<EntityScriptingInterface>()->getEntityPropertiesInternal(id, desiredProperties).getPosition();
+        glm::vec3 keyWorldPosition = DependencyManager::get<EntityScriptingInterface>()->getEntityPropertiesInternal(id, desiredProperties, false).getPosition();
 
         AudioInjectorOptions audioOptions;
         audioOptions.localOnly = true;
@@ -662,7 +662,7 @@ void Keyboard::handleTriggerContinue(const QUuid& id, const PointerEvent& event)
             auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
             EntityPropertyFlags desiredProperties;
             desiredProperties += PROP_ROTATION;
-            glm::quat orientation = entityScriptingInterface->getEntityPropertiesInternal(id, desiredProperties).getRotation();
+            glm::quat orientation = entityScriptingInterface->getEntityPropertiesInternal(id, desiredProperties, false).getRotation();
             glm::vec3 yAxis = orientation * Z_AXIS;
             glm::vec3 yOffset = yAxis * Z_OFFSET;
             glm::vec3 localPosition = key.getCurrentLocalPosition() - yOffset;
diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp
index 8597cb5717..ce5a588776 100644
--- a/interface/src/ui/PreferencesDialog.cpp
+++ b/interface/src/ui/PreferencesDialog.cpp
@@ -100,7 +100,8 @@ void setupPreferences() {
         QStringList refreshRateProfiles
             { QString::fromStdString(RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile::ECO)),
               QString::fromStdString(RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile::INTERACTIVE)),
-              QString::fromStdString(RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile::REALTIME)) };
+              QString::fromStdString(RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile::REALTIME)),
+              QString::fromStdString(RefreshRateManager::refreshRateProfileToString(RefreshRateManager::RefreshRateProfile::CUSTOM)) };
 
         preference->setItems(refreshRateProfiles);
         preferences->addPreference(preference);
diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp
index e245acfd40..5349564043 100644
--- a/interface/src/ui/overlays/Overlays.cpp
+++ b/interface/src/ui/overlays/Overlays.cpp
@@ -153,14 +153,6 @@ void Overlays::render(RenderArgs* renderArgs) {
     }
 }
 
-void Overlays::disable() {
-    _enabled = false;
-}
-
-void Overlays::enable() {
-    _enabled = true;
-}
-
 Overlay::Pointer Overlays::take2DOverlay(const QUuid& id) {
     if (_shuttingDown) {
         return nullptr;
@@ -378,7 +370,7 @@ QObject* Overlays::getOverlayObject(const QUuid& id) {
 }
 
 QUuid Overlays::getOverlayAtPoint(const glm::vec2& point) {
-    if (_shuttingDown || !_enabled) {
+    if (_shuttingDown) {
         return UNKNOWN_ENTITY_ID;
     }
 
diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h
index a0f2e866e2..fcf0c71bc9 100644
--- a/interface/src/ui/overlays/Overlays.h
+++ b/interface/src/ui/overlays/Overlays.h
@@ -99,8 +99,6 @@ public:
     void init();
     void update(float deltatime);
     void render(RenderArgs* renderArgs);
-    void disable();
-    void enable();
 
     Overlay::Pointer take2DOverlay(const QUuid& id);
     Overlay::Pointer get2DOverlay(const QUuid& id) const;
@@ -683,7 +681,6 @@ private:
 
     unsigned int _stackOrder { 1 };
 
-    bool _enabled { true };
     std::atomic<bool> _shuttingDown { false };
 
     PointerEvent calculateOverlayPointerEvent(const QUuid& id, const PickRay& ray, const RayToOverlayIntersectionResult& rayPickResult,
diff --git a/launchers/qt/CMakeLists.txt b/launchers/qt/CMakeLists.txt
index 12cf7f08d4..0e5ddd6990 100644
--- a/launchers/qt/CMakeLists.txt
+++ b/launchers/qt/CMakeLists.txt
@@ -281,7 +281,7 @@ if (APPLE)
 
   set(CPACK_NSIS_DISPLAY_NAME ${_DISPLAY_NAME})
 
-  set(DMG_SUBFOLDER_NAME "Vircadia")
+  set(DMG_SUBFOLDER_NAME "Overte")
   set(ESCAPED_DMG_SUBFOLDER_NAME "")
   set(DMG_SUBFOLDER_ICON "${CMAKE_SOURCE_DIR}/cmake/installer/install-folder.rsrc")
 
diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp
index 6eb77da4d7..16b217bc53 100644
--- a/libraries/audio-client/src/AudioClient.cpp
+++ b/libraries/audio-client/src/AudioClient.cpp
@@ -27,7 +27,9 @@
 #endif
 
 #ifdef WIN32
+#ifndef WIN32_LEAN_AND_MEAN
 #define WIN32_LEAN_AND_MEAN 1
+#endif
 #include <windows.h>
 #include <Mmsystem.h>
 #include <mmdeviceapi.h>
diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp
index c71da50b1a..4068f7c547 100644
--- a/libraries/avatars/src/AvatarData.cpp
+++ b/libraries/avatars/src/AvatarData.cpp
@@ -2106,6 +2106,14 @@ const QUrl& AvatarData::getSkeletonModelURL() const {
     }
 }
 
+QString AvatarData::getSkeletonModelURLFromScript() const {
+    if (isMyAvatar() && !isMyAvatarURLProtected()) {
+        return _skeletonModelURL.toString();
+    }
+
+    return QString();
+};
+
 QByteArray AvatarData::packSkeletonData() const {
     // Send an avatar trait packet with the skeleton data before the mesh is loaded
     int avatarDataSize = 0;
@@ -2558,7 +2566,7 @@ QDataStream& operator>>(QDataStream& in, AttachmentData& attachment) {
 void AttachmentDataObject::setModelURL(const QString& modelURL) {
     AttachmentData data = scriptvalue_cast<AttachmentData>(thisObject());
     data.modelURL = modelURL;
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     thisObject() = engine()->toScriptValue(data);
 }
 
@@ -2569,7 +2577,7 @@ QString AttachmentDataObject::getModelURL() const {
 void AttachmentDataObject::setJointName(const QString& jointName) {
     AttachmentData data = scriptvalue_cast<AttachmentData>(thisObject());
     data.jointName = jointName;
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     thisObject() = engine()->toScriptValue(data);
 }
 
@@ -2580,7 +2588,7 @@ QString AttachmentDataObject::getJointName() const {
 void AttachmentDataObject::setTranslation(const glm::vec3& translation) {
     AttachmentData data = scriptvalue_cast<AttachmentData>(thisObject());
     data.translation = translation;
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     thisObject() = engine()->toScriptValue(data);
 }
 
@@ -2591,7 +2599,7 @@ glm::vec3 AttachmentDataObject::getTranslation() const {
 void AttachmentDataObject::setRotation(const glm::quat& rotation) {
     AttachmentData data = scriptvalue_cast<AttachmentData>(thisObject());
     data.rotation = rotation;
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     thisObject() = engine()->toScriptValue(data);
 }
 
@@ -2602,7 +2610,7 @@ glm::quat AttachmentDataObject::getRotation() const {
 void AttachmentDataObject::setScale(float scale) {
     AttachmentData data = scriptvalue_cast<AttachmentData>(thisObject());
     data.scale = scale;
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     thisObject() = engine()->toScriptValue(data);
 }
 
@@ -2613,7 +2621,7 @@ float AttachmentDataObject::getScale() const {
 void AttachmentDataObject::setIsSoft(bool isSoft) {
     AttachmentData data = scriptvalue_cast<AttachmentData>(thisObject());
     data.isSoft = isSoft;
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     thisObject() = engine()->toScriptValue(data);
 }
 
diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h
index 0b2a925de0..d3bf8a3282 100644
--- a/libraries/avatars/src/AvatarData.h
+++ b/libraries/avatars/src/AvatarData.h
@@ -610,6 +610,8 @@ public:
     AvatarData();
     virtual ~AvatarData();
 
+    virtual bool isMyAvatarURLProtected() const { return false; } // This needs to be here because both MyAvatar and AvatarData inherit from MyAvatar
+
     static const QUrl& defaultFullAvatarModelUrl();
 
     const QUuid getSessionUUID() const { return getID(); }
@@ -1355,7 +1357,7 @@ public:
      */
     Q_INVOKABLE virtual void detachAll(const QString& modelURL, const QString& jointName = QString());
 
-    QString getSkeletonModelURLFromScript() const { return _skeletonModelURL.toString(); }
+    QString getSkeletonModelURLFromScript() const;
     void setSkeletonModelURLFromScript(const QString& skeletonModelString) { setSkeletonModelURL(QUrl(skeletonModelString)); }
 
     void setOwningAvatarMixer(const QWeakPointer<Node>& owningAvatarMixer) { _owningAvatarMixer = owningAvatarMixer; }
diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp
index a07c402555..1d93a6e954 100644
--- a/libraries/avatars/src/ScriptAvatarData.cpp
+++ b/libraries/avatars/src/ScriptAvatarData.cpp
@@ -13,6 +13,7 @@
 
 #include "ScriptAvatarData.h"
 
+#include <NodeList.h>
 #include <ScriptEngineCast.h>
 #include <ScriptManager.h>
 
@@ -204,7 +205,12 @@ bool ScriptAvatarData::getLookAtSnappingEnabled() const {
 //
 QString ScriptAvatarData::getSkeletonModelURLFromScript() const {
     if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) {
-        return sharedAvatarData->getSkeletonModelURLFromScript();
+        auto nodeList = DependencyManager::get<NodeList>();
+        if (sharedAvatarData->isMyAvatar() && !sharedAvatarData->isMyAvatarURLProtected() && nodeList->getThisNodeCanViewAssetURLs()) {
+            return sharedAvatarData->getSkeletonModelURLFromScript();
+        }
+
+        return QString();
     } else {
         return QString();
     }
diff --git a/libraries/baking/src/MaterialBaker.cpp b/libraries/baking/src/MaterialBaker.cpp
index 48718cd6c3..631a10fd11 100644
--- a/libraries/baking/src/MaterialBaker.cpp
+++ b/libraries/baking/src/MaterialBaker.cpp
@@ -36,8 +36,7 @@ MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QStr
     _isURL(isURL),
     _destinationPath(destinationPath),
     _bakedOutputDir(bakedOutputDir),
-    _textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)),
-    _scriptEngine(newScriptEngine())
+    _textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++))
 {
 }
 
@@ -214,16 +213,20 @@ void MaterialBaker::outputMaterial() {
         if (_materialResource->parsedMaterials.networkMaterials.size() == 1) {
             auto networkMaterial = _materialResource->parsedMaterials.networkMaterials.begin();
             auto scriptableMaterial = scriptable::ScriptableMaterial(networkMaterial->second);
-            QVariant materialVariant =
-                scriptable::scriptableMaterialToScriptValue(_scriptEngine.get(), scriptableMaterial).toVariant();
-            json.insert("materials", QJsonDocument::fromVariant(materialVariant).object());
+            _helperScriptEngine.run( [&] {
+                QVariant materialVariant =
+                    scriptable::scriptableMaterialToScriptValue(_helperScriptEngine.get(), scriptableMaterial).toVariant();
+                json.insert("materials", QJsonDocument::fromVariant(materialVariant).object());
+            });
         } else {
             QJsonArray materialArray;
             for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) {
                 auto scriptableMaterial = scriptable::ScriptableMaterial(networkMaterial.second);
-                QVariant materialVariant =
-                    scriptable::scriptableMaterialToScriptValue(_scriptEngine.get(), scriptableMaterial).toVariant();
-                materialArray.append(QJsonDocument::fromVariant(materialVariant).object());
+                _helperScriptEngine.run( [&] {
+                    QVariant materialVariant =
+                        scriptable::scriptableMaterialToScriptValue(_helperScriptEngine.get(), scriptableMaterial).toVariant();
+                    materialArray.append(QJsonDocument::fromVariant(materialVariant).object());
+                });
             }
             json.insert("materials", materialArray);
         }
@@ -269,19 +272,32 @@ void MaterialBaker::setMaterials(const QHash<QString, hfm::Material>& materials,
     _materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
     for (auto& material : materials) {
         _materialResource->parsedMaterials.names.push_back(material.name.toStdString());
-        _materialResource->parsedMaterials.networkMaterials[material.name.toStdString()] = std::make_shared<NetworkMaterial>(material, baseURL);
+        if (!material.isMToonMaterial) {
+            _materialResource->parsedMaterials.networkMaterials[material.name.toStdString()] = std::make_shared<NetworkMaterial>(material, baseURL);
+        } else {
+            _materialResource->parsedMaterials.networkMaterials[material.name.toStdString()] = std::make_shared<NetworkMToonMaterial>(material, baseURL);
+        }
 
         // Store any embedded texture content
         addTexture(material.name, image::TextureUsage::NORMAL_TEXTURE, material.normalTexture);
         addTexture(material.name, image::TextureUsage::ALBEDO_TEXTURE, material.albedoTexture);
-        addTexture(material.name, image::TextureUsage::GLOSS_TEXTURE, material.glossTexture);
-        addTexture(material.name, image::TextureUsage::ROUGHNESS_TEXTURE, material.roughnessTexture);
-        addTexture(material.name, image::TextureUsage::SPECULAR_TEXTURE, material.specularTexture);
-        addTexture(material.name, image::TextureUsage::METALLIC_TEXTURE, material.metallicTexture);
         addTexture(material.name, image::TextureUsage::EMISSIVE_TEXTURE, material.emissiveTexture);
-        addTexture(material.name, image::TextureUsage::OCCLUSION_TEXTURE, material.occlusionTexture);
-        addTexture(material.name, image::TextureUsage::SCATTERING_TEXTURE, material.scatteringTexture);
-        addTexture(material.name, image::TextureUsage::LIGHTMAP_TEXTURE, material.lightmapTexture);
+
+        if (!material.isMToonMaterial) {
+            addTexture(material.name, image::TextureUsage::GLOSS_TEXTURE, material.glossTexture);
+            addTexture(material.name, image::TextureUsage::ROUGHNESS_TEXTURE, material.roughnessTexture);
+            addTexture(material.name, image::TextureUsage::SPECULAR_TEXTURE, material.specularTexture);
+            addTexture(material.name, image::TextureUsage::METALLIC_TEXTURE, material.metallicTexture);
+            addTexture(material.name, image::TextureUsage::OCCLUSION_TEXTURE, material.occlusionTexture);
+            addTexture(material.name, image::TextureUsage::SCATTERING_TEXTURE, material.scatteringTexture);
+            addTexture(material.name, image::TextureUsage::LIGHTMAP_TEXTURE, material.lightmapTexture);
+        } else {
+            addTexture(material.name, image::TextureUsage::ALBEDO_TEXTURE, material.shadeTexture);
+            addTexture(material.name, image::TextureUsage::ROUGHNESS_TEXTURE, material.shadingShiftTexture);
+            addTexture(material.name, image::TextureUsage::EMISSIVE_TEXTURE, material.matcapTexture);
+            addTexture(material.name, image::TextureUsage::ALBEDO_TEXTURE, material.rimTexture);
+            addTexture(material.name, image::TextureUsage::ROUGHNESS_TEXTURE, material.uvAnimationTexture);
+        }
     }
 }
 
diff --git a/libraries/baking/src/MaterialBaker.h b/libraries/baking/src/MaterialBaker.h
index 129b36aa8f..4fd8948b03 100644
--- a/libraries/baking/src/MaterialBaker.h
+++ b/libraries/baking/src/MaterialBaker.h
@@ -21,6 +21,7 @@
 #include "TextureBaker.h"
 #include "baking/TextureFileNamer.h"
 
+#include <HelperScriptEngine.h>
 #include <procedural/ProceduralMaterialCache.h>
 #include <ScriptEngine.h>
 
@@ -72,7 +73,7 @@ private:
     QString _textureOutputDir;
     QString _bakedMaterialData;
 
-    ScriptEnginePointer _scriptEngine;
+    HelperScriptEngine _helperScriptEngine;
     static std::function<QThread*()> _getNextOvenWorkerThreadOperator;
     TextureFileNamer _textureFileNamer;
 
diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp
index 4861bc6ecb..8fba576616 100644
--- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp
+++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp
@@ -232,6 +232,14 @@ void EntityTreeRenderer::resetPersistentEntitiesScriptEngine() {
     _persistentEntitiesScriptManager = scriptManagerFactory(ScriptManager::ENTITY_CLIENT_SCRIPT, NO_SCRIPT,
                                                 QString("about:Entities %1").arg(++_entitiesScriptEngineCount));
     DependencyManager::get<ScriptEngines>()->runScriptInitializers(_persistentEntitiesScriptManager);
+
+    // Make script engine messages available through ScriptDiscoveryService
+    auto scriptEngines = DependencyManager::get<ScriptEngines>().data();
+    connect(_persistentEntitiesScriptManager.get(), &ScriptManager::infoEntityMessage, scriptEngines, &ScriptEngines::infoEntityMessage);
+    connect(_persistentEntitiesScriptManager.get(), &ScriptManager::printedEntityMessage, scriptEngines, &ScriptEngines::printedEntityMessage);
+    connect(_persistentEntitiesScriptManager.get(), &ScriptManager::errorEntityMessage, scriptEngines, &ScriptEngines::errorEntityMessage);
+    connect(_persistentEntitiesScriptManager.get(), &ScriptManager::warningEntityMessage, scriptEngines, &ScriptEngines::warningEntityMessage);
+
     _persistentEntitiesScriptManager->runInThread();
     std::shared_ptr<EntitiesScriptEngineProvider> entitiesScriptEngineProvider = _persistentEntitiesScriptManager;
     auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
@@ -255,6 +263,14 @@ void EntityTreeRenderer::resetNonPersistentEntitiesScriptEngine() {
     _nonPersistentEntitiesScriptManager = scriptManagerFactory(ScriptManager::ENTITY_CLIENT_SCRIPT, NO_SCRIPT,
                                                 QString("about:Entities %1").arg(++_entitiesScriptEngineCount));
     DependencyManager::get<ScriptEngines>()->runScriptInitializers(_nonPersistentEntitiesScriptManager);
+
+    // Make script engine messages available through ScriptDiscoveryService
+    auto scriptEngines = DependencyManager::get<ScriptEngines>().data();
+    connect(_nonPersistentEntitiesScriptManager.get(), &ScriptManager::infoEntityMessage, scriptEngines, &ScriptEngines::infoEntityMessage);
+    connect(_nonPersistentEntitiesScriptManager.get(), &ScriptManager::printedEntityMessage, scriptEngines, &ScriptEngines::printedEntityMessage);
+    connect(_nonPersistentEntitiesScriptManager.get(), &ScriptManager::errorEntityMessage, scriptEngines, &ScriptEngines::errorEntityMessage);
+    connect(_nonPersistentEntitiesScriptManager.get(), &ScriptManager::warningEntityMessage, scriptEngines, &ScriptEngines::warningEntityMessage);
+
     _nonPersistentEntitiesScriptManager->runInThread();
     std::shared_ptr<EntitiesScriptEngineProvider> entitiesScriptEngineProvider = _nonPersistentEntitiesScriptManager;
     DependencyManager::get<EntityScriptingInterface>()->setNonPersistentEntitiesScriptEngine(entitiesScriptEngineProvider);
@@ -912,7 +928,7 @@ QUuid EntityTreeRenderer::mousePressEvent(QMouseEvent* event) {
                                   pos2D, rayPickResult.intersection,
                                   rayPickResult.surfaceNormal, ray.direction,
                                   toPointerButton(*event), toPointerButtons(*event),
-                                  Qt::NoModifier); // TODO -- check for modifier keys?
+                                  event->modifiers());
 
         emit entityScriptingInterface->mousePressOnEntity(rayPickResult.entityID, pointerEvent);
 
@@ -943,9 +959,10 @@ void EntityTreeRenderer::mouseDoublePressEvent(QMouseEvent* event) {
     if (rayPickResult.intersects && (entity = getTree()->findEntityByID(rayPickResult.entityID))) {
         glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult);
         PointerEvent pointerEvent(PointerEvent::Press, PointerManager::MOUSE_POINTER_ID,
-            pos2D, rayPickResult.intersection,
-            rayPickResult.surfaceNormal, ray.direction,
-            toPointerButton(*event), toPointerButtons(*event), Qt::NoModifier);
+                                  pos2D, rayPickResult.intersection,
+                                  rayPickResult.surfaceNormal, ray.direction,
+                                  toPointerButton(*event), toPointerButtons(*event),
+                                  event->modifiers());
 
         emit entityScriptingInterface->mouseDoublePressOnEntity(rayPickResult.entityID, pointerEvent);
 
@@ -979,7 +996,7 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) {
                                   pos2D, rayPickResult.intersection,
                                   rayPickResult.surfaceNormal, ray.direction,
                                   toPointerButton(*event), toPointerButtons(*event),
-                                  Qt::NoModifier); // TODO -- check for modifier keys?
+                                  event->modifiers());
 
         emit entityScriptingInterface->mouseReleaseOnEntity(rayPickResult.entityID, pointerEvent);
 
@@ -995,7 +1012,7 @@ void EntityTreeRenderer::mouseReleaseEvent(QMouseEvent* event) {
                                   pos2D, rayPickResult.intersection,
                                   rayPickResult.surfaceNormal, ray.direction,
                                   toPointerButton(*event), toPointerButtons(*event),
-                                  Qt::NoModifier); // TODO -- check for modifier keys?
+                                  event->modifiers());
 
         emit entityScriptingInterface->clickReleaseOnEntity(_currentClickingOnEntityID, pointerEvent);
     }
@@ -1022,7 +1039,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) {
                                   pos2D, rayPickResult.intersection,
                                   rayPickResult.surfaceNormal, ray.direction,
                                   toPointerButton(*event), toPointerButtons(*event),
-                                  Qt::NoModifier); // TODO -- check for modifier keys?
+                                  event->modifiers());
 
         emit entityScriptingInterface->mouseMoveOnEntity(rayPickResult.entityID, pointerEvent);
 
@@ -1036,7 +1053,7 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) {
                                       pos2D, rayPickResult.intersection,
                                       rayPickResult.surfaceNormal, ray.direction,
                                       toPointerButton(*event), toPointerButtons(*event),
-                                      Qt::NoModifier); // TODO -- check for modifier keys?
+                                      event->modifiers());
 
             emit entityScriptingInterface->hoverLeaveEntity(_currentHoverOverEntityID, pointerEvent);
         }
@@ -1064,10 +1081,10 @@ void EntityTreeRenderer::mouseMoveEvent(QMouseEvent* event) {
         if (!_currentHoverOverEntityID.isInvalidID()) {
             glm::vec2 pos2D = projectOntoEntityXYPlane(entity, ray, rayPickResult);
             PointerEvent pointerEvent(PointerEvent::Move, PointerManager::MOUSE_POINTER_ID,
-                                  pos2D, rayPickResult.intersection,
-                                  rayPickResult.surfaceNormal, ray.direction,
+                                      pos2D, rayPickResult.intersection,
+                                      rayPickResult.surfaceNormal, ray.direction,
                                       toPointerButton(*event), toPointerButtons(*event),
-                                      Qt::NoModifier); // TODO -- check for modifier keys?
+                                      event->modifiers());
 
             emit entityScriptingInterface->hoverLeaveEntity(_currentHoverOverEntityID, pointerEvent);
             _currentHoverOverEntityID = UNKNOWN_ENTITY_ID; // makes it the unknown ID
diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp
index 2b57c8b78a..c68651d42c 100644
--- a/libraries/entities-renderer/src/RenderableEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Brad Hefta-Gaub on 12/6/13.
 //  Copyright 2013 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -202,6 +203,8 @@ ItemKey EntityRenderer::getKey() {
         builder.withInvisible();
     }
 
+    updateItemKeyBuilderFromMaterials(builder);
+
     return builder;
 }
 
@@ -328,6 +331,20 @@ ItemID EntityRenderer::computeMirrorViewOperator(ViewFrustum& viewFrustum, const
     return foundPortalExit ? DependencyManager::get<EntityTreeRenderer>()->renderableIdForEntityId(portalExitID) : Item::INVALID_ITEM_ID;
 }
 
+HighlightStyle EntityRenderer::getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const {
+    std::lock_guard<std::mutex> lock(_materialsLock);
+    auto materials = _materials.find("0");
+    if (materials != _materials.end()) {
+        glm::vec3 position;
+        withReadLock([&] {
+            position = _renderTransform.getTranslation();
+        });
+        return HighlightStyle::calculateOutlineStyle(materials->second.getOutlineWidthMode(), materials->second.getOutlineWidth(),
+                                                     materials->second.getOutline(), position, viewFrustum, height);
+    }
+    return HighlightStyle();
+}
+
 void EntityRenderer::render(RenderArgs* args) {
     if (!isValidRenderItem()) {
         return;
@@ -627,7 +644,7 @@ EntityRenderer::Pipeline EntityRenderer::getPipelineType(const graphics::MultiMa
     }
 
     graphics::MaterialKey drawMaterialKey = materials.getMaterialKey();
-    if (drawMaterialKey.isEmissive() || drawMaterialKey.isMetallic() || drawMaterialKey.isScattering()) {
+    if (materials.isMToon() || drawMaterialKey.isEmissive() || drawMaterialKey.isMetallic() || drawMaterialKey.isScattering()) {
         return Pipeline::MATERIAL;
     }
 
@@ -747,6 +764,26 @@ Item::Bound EntityRenderer::getMaterialBound(RenderArgs* args) {
     return EntityRenderer::getBound(args);
 }
 
+void EntityRenderer::updateItemKeyBuilderFromMaterials(ItemKey::Builder& builder) {
+    MaterialMap::iterator materials;
+    {
+        std::lock_guard<std::mutex> lock(_materialsLock);
+        materials = _materials.find("0");
+
+        if (materials != _materials.end()) {
+            if (materials->second.shouldUpdate()) {
+                RenderPipelines::updateMultiMaterial(materials->second);
+            }
+        } else {
+            return;
+        }
+    }
+
+    if (materials->second.hasOutline()) {
+        builder.withOutline();
+    }
+}
+
 void EntityRenderer::updateShapeKeyBuilderFromMaterials(ShapeKey::Builder& builder) {
     MaterialMap::iterator materials;
     {
@@ -773,7 +810,7 @@ void EntityRenderer::updateShapeKeyBuilderFromMaterials(ShapeKey::Builder& build
     builder.withCullFaceMode(materials->second.getCullFaceMode());
 
     graphics::MaterialKey drawMaterialKey = materials->second.getMaterialKey();
-    if (drawMaterialKey.isUnlit()) {
+    if (!materials->second.isMToon() && drawMaterialKey.isUnlit()) {
         builder.withUnlit();
     }
 
@@ -783,8 +820,12 @@ void EntityRenderer::updateShapeKeyBuilderFromMaterials(ShapeKey::Builder& build
         if (drawMaterialKey.isNormalMap()) {
             builder.withTangents();
         }
-        if (drawMaterialKey.isLightMap()) {
-            builder.withLightMap();
+        if (!materials->second.isMToon()) {
+            if (drawMaterialKey.isLightMap()) {
+                builder.withLightMap();
+            }
+        } else {
+            builder.withMToon();
         }
     } else if (pipelineType == Pipeline::PROCEDURAL) {
         builder.withOwnPipeline();
diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h
index 495eeea220..949590c472 100644
--- a/libraries/entities-renderer/src/RenderableEntityItem.h
+++ b/libraries/entities-renderer/src/RenderableEntityItem.h
@@ -4,6 +4,7 @@
 //
 //  Created by Brad Hefta-Gaub on 12/6/13.
 //  Copyright 2013 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -13,6 +14,8 @@
 #define hifi_RenderableEntityItem_h
 
 #include <render/Scene.h>
+#include <render/HighlightStyle.h>
+
 #include <EntityItem.h>
 #include <Sound.h>
 #include "AbstractViewStateInterface.h"
@@ -79,6 +82,7 @@ public:
     static ItemID computeMirrorViewOperator(ViewFrustum& viewFrustum, const glm::vec3& inPropertiesPosition, const glm::quat& inPropertiesRotation,
                                             MirrorMode mirrorMode, const QUuid& portalExitID);
     virtual void renderSimulate(RenderArgs* args) override {}
+    virtual HighlightStyle getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const override;
 
 protected:
     virtual bool needsRenderUpdateFromEntity() const final { return needsRenderUpdateFromEntity(_entity); }
@@ -138,6 +142,7 @@ protected:
     void updateMaterials(bool baseMaterialChanged = false);
     bool materialsTransparent() const;
     Item::Bound getMaterialBound(RenderArgs* args);
+    void updateItemKeyBuilderFromMaterials(ItemKey::Builder& builder);
     void updateShapeKeyBuilderFromMaterials(ShapeKey::Builder& builder);
 
     Item::Bound _bound;
diff --git a/libraries/entities-renderer/src/RenderableImageEntityItem.cpp b/libraries/entities-renderer/src/RenderableImageEntityItem.cpp
index 66a5d0d609..0b76038cd4 100644
--- a/libraries/entities-renderer/src/RenderableImageEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableImageEntityItem.cpp
@@ -130,8 +130,7 @@ void ImageEntityRenderer::doRender(RenderArgs* args) {
         materials = _materials["0"];
     }
 
-    auto& schema = materials.getSchemaBuffer().get<graphics::MultiMaterial::Schema>();
-    glm::vec4 color = glm::vec4(ColorUtils::tosRGBVec3(schema._albedo), schema._opacity);
+    glm::vec4 color = materials.getColor();
     color = EntityRenderer::calculatePulseColor(color, _pulseProperties, _created);
 
     if (!_texture || !_texture->isLoaded() || color.a == 0.0f) {
diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp
index fe44c41094..576e842f84 100644
--- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.cpp
@@ -1,6 +1,7 @@
 //
 //  Created by Sam Gondelman on 1/18/2018
 //  Copyright 2018 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -219,7 +220,7 @@ void MaterialEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPo
 ItemKey MaterialEntityRenderer::getKey() {
     auto builder = ItemKey::Builder().withTypeShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
 
-    if (!_visible) {
+    if (!_visible || !_parentID.isNull()) {
         builder.withInvisible();
     }
 
@@ -229,6 +230,10 @@ ItemKey MaterialEntityRenderer::getKey() {
         if (matKey.isTranslucent()) {
             builder.withTransparent();
         }
+
+        if (drawMaterial->getOutlineWidthMode() != NetworkMToonMaterial::OutlineWidthMode::OUTLINE_NONE && drawMaterial->getOutlineWidth() > 0.0f) {
+            builder.withOutline();
+        }
     }
 
     return builder.build();
@@ -258,11 +263,16 @@ ShapeKey MaterialEntityRenderer::getShapeKey() {
         if (drawMaterialKey.isNormalMap()) {
             builder.withTangents();
         }
-        if (drawMaterialKey.isLightMap()) {
-            builder.withLightMap();
-        }
-        if (drawMaterialKey.isUnlit()) {
-            builder.withUnlit();
+
+        if (drawMaterial && drawMaterial->isMToon()) {
+            builder.withMToon();
+        } else {
+            if (drawMaterialKey.isLightMap()) {
+                builder.withLightMap();
+            }
+            if (drawMaterialKey.isUnlit()) {
+                builder.withUnlit();
+            }
         }
     }
 
@@ -273,6 +283,18 @@ ShapeKey MaterialEntityRenderer::getShapeKey() {
     return builder.build();
 }
 
+HighlightStyle MaterialEntityRenderer::getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const {
+    if (const auto drawMaterial = getMaterial()) {
+        glm::vec3 position;
+        withReadLock([&] {
+            position = _renderTransform.getTranslation();
+        });
+        return HighlightStyle::calculateOutlineStyle(drawMaterial->getOutlineWidthMode(), drawMaterial->getOutlineWidth(),
+                                                     drawMaterial->getOutline(), position, viewFrustum, height);
+    }
+    return HighlightStyle();
+}
+
 void MaterialEntityRenderer::doRender(RenderArgs* args) {
     PerformanceTimer perfTimer("RenderableMaterialEntityItem::render");
     Q_ASSERT(args->_batch);
@@ -316,21 +338,26 @@ void MaterialEntityRenderer::doRender(RenderArgs* args) {
         }
 
         // Draw!
-        DependencyManager::get<GeometryCache>()->renderSphere(batch);
+        const uint32_t compactColor = 0xFFFFFFFF;
+        _colorBuffer->setData(sizeof(compactColor), (const gpu::Byte*) &compactColor);
+        DependencyManager::get<GeometryCache>()->renderShape(batch, GeometryCache::Shape::Sphere, _colorBuffer);
     } else {
         auto proceduralDrawMaterial = std::static_pointer_cast<graphics::ProceduralMaterial>(drawMaterial);
         glm::vec4 outColor = glm::vec4(drawMaterial->getAlbedo(), drawMaterial->getOpacity());
         outColor = proceduralDrawMaterial->getColor(outColor);
         proceduralDrawMaterial->prepare(batch, transform.getTranslation(), transform.getScale(),
                                         transform.getRotation(), _created, ProceduralProgramKey(outColor.a < 1.0f));
+
+        const uint32_t compactColor = GeometryCache::toCompactColor(glm::vec4(outColor));
+        _colorBuffer->setData(sizeof(compactColor), (const gpu::Byte*) &compactColor);
         if (render::ShapeKey(args->_globalShapeKey).isWireframe() || _primitiveMode == PrimitiveMode::LINES) {
-            DependencyManager::get<GeometryCache>()->renderWireSphere(batch, outColor);
+            DependencyManager::get<GeometryCache>()->renderWireShape(batch, GeometryCache::Shape::Sphere, _colorBuffer);
         } else {
-            DependencyManager::get<GeometryCache>()->renderSphere(batch, outColor);
+            DependencyManager::get<GeometryCache>()->renderShape(batch, GeometryCache::Shape::Sphere, _colorBuffer);
         }
     }
 
-    args->_details._trianglesRendered += (int)DependencyManager::get<GeometryCache>()->getSphereTriangleCount();
+    args->_details._trianglesRendered += (int)DependencyManager::get<GeometryCache>()->getShapeTriangleCount(GeometryCache::Shape::Sphere);
 }
 
 void MaterialEntityRenderer::setCurrentMaterialName(const std::string& currentMaterialName) {
diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.h b/libraries/entities-renderer/src/RenderableMaterialEntityItem.h
index 25403e8a2b..efded3aab3 100644
--- a/libraries/entities-renderer/src/RenderableMaterialEntityItem.h
+++ b/libraries/entities-renderer/src/RenderableMaterialEntityItem.h
@@ -1,6 +1,7 @@
 //
 //  Created by Sam Gondelman on 1/18/2018
 //  Copyright 2018 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -32,6 +33,7 @@ private:
     virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
     virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override;
     virtual void doRender(RenderArgs* args) override;
+    virtual HighlightStyle getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const override;
 
     ItemKey getKey() override;
     ShapeKey getShapeKey() override;
@@ -65,6 +67,7 @@ private:
     std::shared_ptr<NetworkMaterial> _appliedMaterial;
     std::string _currentMaterialName;
 
+    gpu::BufferPointer _colorBuffer { std::make_shared<gpu::Buffer>() };
 };
 
 } } 
diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp
index 8fa787d413..6393f76603 100644
--- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp
@@ -21,23 +21,48 @@ using namespace render::entities;
 
 static uint8_t CUSTOM_PIPELINE_NUMBER = 0;
 static gpu::Stream::FormatPointer _vertexFormat;
-static std::weak_ptr<gpu::Pipeline> _texturedPipeline;
+// forward, transparent, shadow, wireframe
+static std::map<std::tuple<bool, bool, bool, bool>, gpu::PipelinePointer> _pipelines;
 
 static ShapePipelinePointer shapePipelineFactory(const ShapePlumber& plumber, const ShapeKey& key, RenderArgs* args) {
-    auto texturedPipeline = _texturedPipeline.lock();
-    if (!texturedPipeline) {
-        auto state = std::make_shared<gpu::State>();
-        state->setCullMode(gpu::State::CULL_BACK);
-        state->setDepthTest(true, false, gpu::LESS_EQUAL);
-        state->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE,
-            gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
-        PrepareStencil::testMask(*state);
+    if (_pipelines.empty()) {
+        using namespace shader::entities_renderer::program;
 
-        auto program = gpu::Shader::createProgram(shader::entities_renderer::program::textured_particle);
-        _texturedPipeline = texturedPipeline = gpu::Pipeline::create(program, state);
+        // forward, translucent, shadow
+        static const std::vector<std::tuple<bool, bool, bool, uint32_t>> keys = {
+            std::make_tuple(false, false, false, textured_particle),
+            std::make_tuple(true, false, false, textured_particle_forward),
+            std::make_tuple(false, true, false, textured_particle_translucent),
+            std::make_tuple(true, true, false, textured_particle_translucent_forward),
+            std::make_tuple(false, false, true, textured_particle_shadow),
+            // no such thing as shadow and forward/translucent
+        };
+
+        for (auto& key : keys) {
+            for (int i = 0; i < 2; ++i) {
+                bool transparent = std::get<1>(key);
+                bool wireframe = i == 0;
+
+                auto state = std::make_shared<gpu::State>();
+                state->setCullMode(gpu::State::CULL_BACK);
+
+                if (wireframe) {
+                    state->setFillMode(gpu::State::FILL_LINE);
+                }
+
+                state->setDepthTest(true, !transparent, gpu::LESS_EQUAL);
+                state->setBlendFunction(transparent, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE,
+                    gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE);
+                transparent ? PrepareStencil::testMask(*state) : PrepareStencil::testMaskDrawShape(*state);
+
+                auto program = gpu::Shader::createProgram(std::get<3>(key));
+                _pipelines[std::make_tuple(std::get<0>(key), transparent, std::get<2>(key), wireframe)] = gpu::Pipeline::create(program, state);
+            }
+        }
     }
 
-    return std::make_shared<render::ShapePipeline>(texturedPipeline, nullptr, nullptr, nullptr);
+    return std::make_shared<render::ShapePipeline>(_pipelines[std::make_tuple(args->_renderMethod == Args::RenderMethod::FORWARD, key.isTranslucent(),
+        args->_renderMode == Args::RenderMode::SHADOW_RENDER_MODE, key.isWireframe())], nullptr, nullptr, nullptr);
 }
 
 struct GpuParticle {
@@ -138,28 +163,31 @@ void ParticleEffectEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEn
     _uniformBuffer.edit<ParticleUniforms>() = particleUniforms;
 }
 
+bool ParticleEffectEntityRenderer::isTransparent() const {
+    bool particleTransparent = _particleProperties.getColorStart().a < 1.0f || _particleProperties.getColorMiddle().a < 1.0f ||
+                               _particleProperties.getColorFinish().a < 1.0f || _particleProperties.getColorSpread().a > 0.0f ||
+                               _pulseProperties.getAlphaMode() != PulseMode::NONE || (_textureLoaded && _networkTexture && _networkTexture->getGPUTexture() &&
+                                   _networkTexture->getGPUTexture()->getUsage().isAlpha() && !_networkTexture->getGPUTexture()->getUsage().isAlphaMask());
+    return particleTransparent || Parent::isTransparent();
+}
+
 ItemKey ParticleEffectEntityRenderer::getKey() {
-    // FIXME: implement isTransparent() for particles and an opaque pipeline
-    auto builder = ItemKey::Builder::transparentShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
-
-    if (!_visible) {
-        builder.withInvisible();
-    }
-
-    if (_cullWithParent) {
-        builder.withSubMetaCulled();
-    }
-
+    auto builder = ItemKey::Builder(Parent::getKey());
     builder.withSimulate();
-
     return builder.build();
 }
 
 ShapeKey ParticleEffectEntityRenderer::getShapeKey() {
-    auto builder = ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER).withTranslucent();
+    auto builder = ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER);
+
+    if (isTransparent()) {
+        builder.withTranslucent();
+    }
+
     if (_primitiveMode == PrimitiveMode::LINES) {
         builder.withWireframe();
     }
+
     return builder.build();
 }
 
diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h
index b3414594c3..d9a1745b06 100644
--- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h
+++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h
@@ -30,6 +30,7 @@ protected:
     virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
     virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override;
 
+    bool isTransparent() const override;
     virtual ItemKey getKey() override;
     virtual ShapeKey getShapeKey() override;
     virtual Item::Bound getBound(RenderArgs* args) override;
diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp
index 26091a1ed4..bda800abe2 100644
--- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.cpp
@@ -1719,21 +1719,28 @@ using namespace render;
 using namespace render::entities;
 
 static uint8_t CUSTOM_PIPELINE_NUMBER;
-static std::map<std::tuple<bool, bool, bool>, ShapePipelinePointer> _pipelines;
+// forward, shadow, fade, wireframe
+static std::map<std::tuple<bool, bool, bool, bool>, ShapePipelinePointer> _pipelines;
 static gpu::Stream::FormatPointer _vertexFormat;
 
 ShapePipelinePointer shapePipelineFactory(const ShapePlumber& plumber, const ShapeKey& key, RenderArgs* args) {
-    // FIXME: custom pipelines like this don't handle shadows or renderLayers correctly
-
     if (_pipelines.empty()) {
         using namespace shader::entities_renderer::program;
 
-        static const std::vector<std::tuple<bool, bool, uint32_t>> keys = {
-            std::make_tuple(false, false, polyvox), std::make_tuple(true, false, polyvox_forward)
+        // forward, shadow, fade
+        static const std::vector<std::tuple<bool, bool, bool, uint32_t>> keys = {
+            std::make_tuple(false, false, false, polyvox),
+            std::make_tuple(true, false, false, polyvox_forward),
+            std::make_tuple(false, true, false, polyvox_shadow),
+            // no such thing as forward + shadow
 #ifdef POLYVOX_ENTITY_USE_FADE_EFFECT
-            , std::make_tuple(false, true, polyvox_fade), std::make_tuple(true, true, polyvox_forward_fade)
+            std::make_tuple(false, false, true, polyvox_fade),
+            std::make_tuple(false, true, true, polyvox_shadow_fade),
+            // no such thing as forward + fade/shadow
 #else
-            , std::make_tuple(false, true, polyvox), std::make_tuple(true, true, polyvox_forward)
+            std::make_tuple(false, false, true, polyvox),
+            std::make_tuple(false, true, true, polyvox_shadow),
+            // no such thing as forward + fade/shadow
 #endif
         };
         for (auto& key : keys) {
@@ -1749,19 +1756,19 @@ ShapePipelinePointer shapePipelineFactory(const ShapePlumber& plumber, const Sha
                     state->setFillMode(gpu::State::FILL_LINE);
                 }
 
-                auto pipeline = gpu::Pipeline::create(gpu::Shader::createProgram(std::get<2>(key)), state);
-                if (std::get<1>(key)) {
-                    _pipelines[std::make_tuple(std::get<0>(key), std::get<1>(key), wireframe)] = std::make_shared<render::ShapePipeline>(pipeline, nullptr, nullptr, nullptr);
+                auto pipeline = gpu::Pipeline::create(gpu::Shader::createProgram(std::get<3>(key)), state);
+                if (!std::get<2>(key)) {
+                    _pipelines[std::make_tuple(std::get<0>(key), std::get<1>(key), std::get<2>(key), wireframe)] = std::make_shared<render::ShapePipeline>(pipeline, nullptr, nullptr, nullptr);
                 } else {
                     const auto& fadeEffect = DependencyManager::get<FadeEffect>();
-                    _pipelines[std::make_tuple(std::get<0>(key), std::get<1>(key), wireframe)] = std::make_shared<render::ShapePipeline>(pipeline, nullptr,
+                    _pipelines[std::make_tuple(std::get<0>(key), std::get<1>(key), std::get<2>(key), wireframe)] = std::make_shared<render::ShapePipeline>(pipeline, nullptr,
                         fadeEffect->getBatchSetter(), fadeEffect->getItemUniformSetter());
                 }
             }
         }
     }
 
-    return _pipelines[std::make_tuple(args->_renderMethod == Args::RenderMethod::FORWARD, key.isFaded(), key.isWireframe())];
+    return _pipelines[std::make_tuple(args->_renderMethod == Args::RenderMethod::FORWARD, args->_renderMode == Args::RenderMode::SHADOW_RENDER_MODE, key.isFaded(), key.isWireframe())];
 }
 
 PolyVoxEntityRenderer::PolyVoxEntityRenderer(const EntityItemPointer& entity) : Parent(entity) {
@@ -1775,16 +1782,6 @@ PolyVoxEntityRenderer::PolyVoxEntityRenderer(const EntityItemPointer& entity) :
     _params = std::make_shared<gpu::Buffer>(sizeof(glm::vec4), nullptr);
 }
 
-ItemKey PolyVoxEntityRenderer::getKey() {
-    auto builder = ItemKey::Builder::opaqueShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer());
-
-    if (_cullWithParent) {
-        builder.withSubMetaCulled();
-    }
-
-    return builder.build();
-}
-
 ShapeKey PolyVoxEntityRenderer::getShapeKey() {
     auto builder = ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER);
     if (_primitiveMode == PrimitiveMode::LINES) {
@@ -1867,13 +1864,7 @@ void PolyVoxEntityRenderer::doRender(RenderArgs* args) {
     batch.setModelTransform(transform);
 
     batch.setInputFormat(_vertexFormat);
-    batch.setInputBuffer(gpu::Stream::POSITION, _mesh->getVertexBuffer()._buffer, 0,
-        sizeof(PolyVox::PositionMaterialNormal));
-
-    // TODO -- should we be setting this?
-    // batch.setInputBuffer(gpu::Stream::NORMAL, mesh->getVertexBuffer()._buffer,
-    //                      12,
-    //                      sizeof(PolyVox::PositionMaterialNormal));
+    batch.setInputBuffer(gpu::Stream::POSITION, _mesh->getVertexBuffer()._buffer, 0, sizeof(PolyVox::PositionMaterialNormal));
     batch.setIndexBuffer(gpu::UINT32, _mesh->getIndexBuffer()._buffer, 0);
 
     for (size_t i = 0; i < _xyzTextures.size(); ++i) {
diff --git a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h
index c1c35a21c8..1debeb957c 100644
--- a/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h
+++ b/libraries/entities-renderer/src/RenderablePolyVoxEntityItem.h
@@ -203,7 +203,6 @@ public:
     }
 
 protected:
-    virtual ItemKey getKey() override;
     virtual ShapeKey getShapeKey() override;
     virtual bool needsRenderUpdateFromTypedEntity(const TypedEntityPointer& entity) const override;
     virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override;
diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
index 82350f54bf..2286045d5e 100644
--- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
@@ -99,8 +99,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) {
         materials = _materials["0"];
     }
 
-    auto& schema = materials.getSchemaBuffer().get<graphics::MultiMaterial::Schema>();
-    glm::vec4 outColor = glm::vec4(ColorUtils::tosRGBVec3(schema._albedo), schema._opacity);
+    glm::vec4 outColor = materials.getColor();
     outColor = EntityRenderer::calculatePulseColor(outColor, _pulseProperties, _created);
 
     if (outColor.a == 0.0f) {
@@ -133,10 +132,12 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) {
             procedural->prepare(batch, transform.getTranslation(), transform.getScale(), transform.getRotation(), _created, ProceduralProgramKey(outColor.a < 1.0f));
         });
 
+        const uint32_t compactColor = GeometryCache::toCompactColor(glm::vec4(outColor));
+        _colorBuffer->setData(sizeof(compactColor), (const gpu::Byte*) &compactColor);
         if (wireframe) {
-            geometryCache->renderWireShape(batch, geometryShape, outColor);
+            geometryCache->renderWireShape(batch, geometryShape, _colorBuffer);
         } else {
-            geometryCache->renderShape(batch, geometryShape, outColor);
+            geometryCache->renderShape(batch, geometryShape, _colorBuffer);
         }
     } else if (pipelineType == Pipeline::SIMPLE) {
         // FIXME, support instanced multi-shape rendering using multidraw indirect
@@ -151,10 +152,12 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) {
                 geometryCache->renderSolidShapeInstance(args, batch, geometryShape, outColor, pipeline);
             }
         } else {
+            const uint32_t compactColor = GeometryCache::toCompactColor(glm::vec4(outColor));
+            _colorBuffer->setData(sizeof(compactColor), (const gpu::Byte*) &compactColor);
             if (wireframe) {
-                geometryCache->renderWireShape(batch, geometryShape, outColor);
+                geometryCache->renderWireShape(batch, geometryShape, _colorBuffer);
             } else {
-                geometryCache->renderShape(batch, geometryShape, outColor);
+                geometryCache->renderShape(batch, geometryShape, _colorBuffer);
             }
         }
     } else {
@@ -162,7 +165,9 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) {
             args->_details._materialSwitches++;
         }
 
-        geometryCache->renderShape(batch, geometryShape);
+        const uint32_t compactColor = GeometryCache::toCompactColor(glm::vec4(outColor));
+        _colorBuffer->setData(sizeof(compactColor), (const gpu::Byte*) &compactColor);
+        geometryCache->renderShape(batch, geometryShape, _colorBuffer);
     }
 
     const auto triCount = geometryCache->getShapeTriangleCount(geometryShape);
@@ -179,7 +184,7 @@ scriptable::ScriptableModelBase ShapeEntityRenderer::getScriptableModel()  {
         result.appendMaterials(_materials);
         auto materials = _materials.find("0");
         if (materials != _materials.end()) {
-            vertexColor = ColorUtils::tosRGBVec3(materials->second.getSchemaBuffer().get<graphics::MultiMaterial::Schema>()._albedo);
+            vertexColor = materials->second.getColor();
         }
     }
     if (auto mesh = geometryCache->meshFromShape(geometryShape, vertexColor)) {
diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.h b/libraries/entities-renderer/src/RenderableShapeEntityItem.h
index 686014f4de..fd7bd4795b 100644
--- a/libraries/entities-renderer/src/RenderableShapeEntityItem.h
+++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.h
@@ -42,6 +42,8 @@ private:
     std::shared_ptr<graphics::ProceduralMaterial> _material { std::make_shared<graphics::ProceduralMaterial>() };
     glm::vec3 _color { NAN };
     float _alpha { NAN };
+
+    gpu::BufferPointer _colorBuffer { std::make_shared<gpu::Buffer>() };
 };
 
 } } 
diff --git a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp
index a15e2839a4..1cfab07986 100644
--- a/libraries/entities-renderer/src/RenderableTextEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp
@@ -147,8 +147,7 @@ void TextEntityRenderer::doRender(RenderArgs* args) {
         materials = _materials["0"];
     }
 
-    auto& schema = materials.getSchemaBuffer().get<graphics::MultiMaterial::Schema>();
-    glm::vec4 backgroundColor = glm::vec4(ColorUtils::tosRGBVec3(schema._albedo), schema._opacity);
+    glm::vec4 backgroundColor = materials.getColor();
     backgroundColor = EntityRenderer::calculatePulseColor(backgroundColor, _pulseProperties, _created);
 
     if (backgroundColor.a <= 0.0f) {
diff --git a/libraries/entities-renderer/src/entities-renderer/textured_particle.slp b/libraries/entities-renderer/src/entities-renderer/textured_particle.slp
index e69de29bb2..80b57dee25 100644
--- a/libraries/entities-renderer/src/entities-renderer/textured_particle.slp
+++ b/libraries/entities-renderer/src/entities-renderer/textured_particle.slp
@@ -0,0 +1 @@
+DEFINES (translucent:f forward:f)/shadow:f
\ No newline at end of file
diff --git a/libraries/entities-renderer/src/textured_particle.slf b/libraries/entities-renderer/src/textured_particle.slf
index 7dadb6fc49..04b74771a0 100644
--- a/libraries/entities-renderer/src/textured_particle.slf
+++ b/libraries/entities-renderer/src/textured_particle.slf
@@ -15,8 +15,34 @@ LAYOUT(binding=0) uniform sampler2D colorMap;
 layout(location=0) in vec4 varColor;
 layout(location=1) in vec2 varTexcoord;
 
-layout(location=0) out vec4 outFragColor;
+<@if HIFI_USE_FORWARD or HIFI_USE_SHADOW@>
+    layout(location=0) out vec4 _fragColor0;
+<@else@>
+    <@include DeferredBufferWrite.slh@>
+<@endif@>
 
 void main(void) {
-    outFragColor = texture(colorMap, varTexcoord.xy) * varColor;
+    vec4 albedo = texture(colorMap, varTexcoord.xy) * varColor;
+
+<@if HIFI_USE_FORWARD or HIFI_USE_SHADOW@>
+    <@if not HIFI_USE_TRANSLUCENT@>
+        // to reduce texel flickering for floating point error we discard when alpha is "almost one"
+        if (albedo.a < 0.999999) {
+            discard;
+        }
+    <@endif@>
+
+<@if HIFI_USE_FORWARD@>
+    _fragColor0 = albedo;
+<@else@>
+    _fragColor0 = vec4(1.0);
+<@endif@>
+<@else@>
+    vec3 NORMAL = vec3(1.0, 0.0, 0.0);
+    <@if not HIFI_USE_TRANSLUCENT@>
+        packDeferredFragmentUnlit(NORMAL, albedo.a, albedo.rgb);
+    <@else@>
+        packDeferredFragmentTranslucent(NORMAL, albedo.a, albedo.rgb, DEFAULT_ROUGHNESS);
+    <@endif@>
+<@endif@>
 }
diff --git a/libraries/entities-renderer/src/textured_particle.slv b/libraries/entities-renderer/src/textured_particle.slv
index 98d25eae2e..9fd5e87d32 100644
--- a/libraries/entities-renderer/src/textured_particle.slv
+++ b/libraries/entities-renderer/src/textured_particle.slv
@@ -152,9 +152,9 @@ void main(void) {
     <$transformModelToWorldDir(cam, obj, UP, modelUpWorld)$>
     vec3 upWorld = mix(UP, normalize(modelUpWorld), float(particle.rotateWithEntity));
     vec3 upEye = normalize(view3 * upWorld);
-    vec3 FORWARD = vec3(0, 0, -1);
-    vec3 particleRight = normalize(cross(FORWARD, upEye));
-    vec3 particleUp = cross(particleRight, FORWARD);     // don't need to normalize
+    vec3 eyeToParticle = normalize(anchorPoint.xyz - vec3(0.0));
+    vec3 particleRight = cross(eyeToParticle, upEye);
+    vec3 particleUp = cross(particleRight, eyeToParticle);     // don't need to normalize
     // This ordering ensures that un-rotated particles render upright in the viewer.
     vec3 UNIT_QUAD[NUM_VERTICES_PER_PARTICLE] = vec3[NUM_VERTICES_PER_PARTICLE](
         normalize(-particleRight + particleUp),
diff --git a/libraries/entities/src/AmbientLightPropertyGroup.cpp b/libraries/entities/src/AmbientLightPropertyGroup.cpp
index 829d8ecdf6..c430688113 100644
--- a/libraries/entities/src/AmbientLightPropertyGroup.cpp
+++ b/libraries/entities/src/AmbientLightPropertyGroup.cpp
@@ -22,10 +22,12 @@
 const float AmbientLightPropertyGroup::DEFAULT_AMBIENT_LIGHT_INTENSITY = 0.5f;
 
 void AmbientLightPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
-    ScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const {
-    
+    ScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+    bool isMyOwnAvatarEntity) const {
+
+    auto nodeList = DependencyManager::get<NodeList>();
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_AMBIENT_LIGHT_INTENSITY, AmbientLight, ambientLight, AmbientIntensity, ambientIntensity);
-    COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_AMBIENT_LIGHT_URL, AmbientLight, ambientLight, AmbientURL, ambientURL);
+    COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_AMBIENT_LIGHT_URL, AmbientLight, ambientLight, AmbientURL, ambientURL);
 }
 
 void AmbientLightPropertyGroup::copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) {
diff --git a/libraries/entities/src/AmbientLightPropertyGroup.h b/libraries/entities/src/AmbientLightPropertyGroup.h
index 52451a6f8b..67597d1713 100644
--- a/libraries/entities/src/AmbientLightPropertyGroup.h
+++ b/libraries/entities/src/AmbientLightPropertyGroup.h
@@ -43,7 +43,8 @@ public:
     // EntityItemProperty related helpers
     virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
                                    ScriptEngine* engine, bool skipDefaults,
-                                   EntityItemProperties& defaultEntityProperties) const override;
+                                   EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                   bool isMyOwnAvatarEntity) const override;
     virtual void copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) override;
 
     void merge(const AmbientLightPropertyGroup& other);
diff --git a/libraries/entities/src/AnimationPropertyGroup.cpp b/libraries/entities/src/AnimationPropertyGroup.cpp
index e44bc135e1..b1b5c08295 100644
--- a/libraries/entities/src/AnimationPropertyGroup.cpp
+++ b/libraries/entities/src/AnimationPropertyGroup.cpp
@@ -57,8 +57,11 @@ bool operator==(const AnimationPropertyGroup& a, const AnimationPropertyGroup& b
  * @property {boolean} smoothFrames=true - <code>true</code> if the frames of the animation should be linearly interpolated to
  *     create smoother movement, <code>false</code> if the frames should not be interpolated.
  */
-void AnimationPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, ScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const {
-    COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_URL, Animation, animation, URL, url);
+void AnimationPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, ScriptEngine* engine,
+                                               bool skipDefaults, EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                               bool isMyOwnAvatarEntity) const {
+    auto nodeList = DependencyManager::get<NodeList>();
+    COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_ANIMATION_URL, Animation, animation, URL, url);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_ALLOW_TRANSLATION, Animation, animation, AllowTranslation, allowTranslation);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_FPS, Animation, animation, FPS, fps);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_ANIMATION_FRAME_INDEX, Animation, animation, CurrentFrame, currentFrame);
diff --git a/libraries/entities/src/AnimationPropertyGroup.h b/libraries/entities/src/AnimationPropertyGroup.h
index b90417d78e..875a1f7126 100644
--- a/libraries/entities/src/AnimationPropertyGroup.h
+++ b/libraries/entities/src/AnimationPropertyGroup.h
@@ -37,7 +37,8 @@ public:
     // EntityItemProperty related helpers
     virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
                                    ScriptEngine* engine, bool skipDefaults,
-                                   EntityItemProperties& defaultEntityProperties) const override;
+                                   EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                   bool isMyOwnAvatarEntity) const override;
     virtual void copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) override;
 
     void merge(const AnimationPropertyGroup& other);
diff --git a/libraries/entities/src/BloomPropertyGroup.cpp b/libraries/entities/src/BloomPropertyGroup.cpp
index f785dc7465..18ad85d797 100644
--- a/libraries/entities/src/BloomPropertyGroup.cpp
+++ b/libraries/entities/src/BloomPropertyGroup.cpp
@@ -18,7 +18,9 @@
 #include "EntityItemProperties.h"
 #include "EntityItemPropertiesMacros.h"
 
-void BloomPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, ScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const {
+void BloomPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, ScriptEngine* engine,
+                                           bool skipDefaults, EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                           bool isMyOwnAvatarEntity) const {
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_BLOOM_INTENSITY, Bloom, bloom, BloomIntensity, bloomIntensity);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_BLOOM_THRESHOLD, Bloom, bloom, BloomThreshold, bloomThreshold);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_BLOOM_SIZE, Bloom, bloom, BloomSize, bloomSize);
diff --git a/libraries/entities/src/BloomPropertyGroup.h b/libraries/entities/src/BloomPropertyGroup.h
index 44b2d18a39..d459bb2f3c 100644
--- a/libraries/entities/src/BloomPropertyGroup.h
+++ b/libraries/entities/src/BloomPropertyGroup.h
@@ -44,7 +44,8 @@ public:
     // EntityItemProperty related helpers
     virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
                                    ScriptEngine* engine, bool skipDefaults,
-                                   EntityItemProperties& defaultEntityProperties) const override;
+                                   EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                   bool isMyOwnAvatarEntity) const override;
     virtual void copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) override;
 
     void merge(const BloomPropertyGroup& other);
diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp
index 60559e54c7..54d20301ae 100644
--- a/libraries/entities/src/EntityItemProperties.cpp
+++ b/libraries/entities/src/EntityItemProperties.cpp
@@ -33,6 +33,7 @@
 #include <Extents.h>
 #include <VariantMapToScriptValue.h>
 #include <ScriptValue.h>
+#include <PhysicsHelpers.h>
 
 #include "EntitiesLogging.h"
 #include "EntityItem.h"
@@ -1430,7 +1431,8 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
  * @property {boolean} unlit=false - <code>true</code> if the entity is unaffected by lighting, <code>false</code> if it is lit
  *     by the key light and local lights.
  * @property {string} font="" - The font to render the text with. It can be one of the following: <code>"Courier"</code>,
- *     <code>"Inconsolata"</code>, <code>"Roboto"</code>, <code>"Timeless"</code>, or a path to a .sdff file.
+ *     <code>"Inconsolata"</code>, <code>"Roboto"</code>, <code>"Timeless"</code>, or a path to a PNG MTSDF .arfont file generated
+ *     by the msdf-atlas-gen tool (https://github.com/Chlumsky/msdf-atlas-gen).
  * @property {Entities.TextEffect} textEffect="none" - The effect that is applied to the text.
  * @property {Color} textEffectColor=255,255,255 - The color of the effect.
  * @property {number} textEffectThickness=0.2 - The magnitude of the text effect, range <code>0.0</code> &ndash; <code>0.5</code>.
@@ -1665,13 +1667,15 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
 
     const bool pseudoPropertyFlagsActive = pseudoPropertyFlags.test(EntityPseudoPropertyFlag::FlagsActive);
     // Fix to skip the default return all mechanism, when pseudoPropertyFlagsActive
-    const bool pseudoPropertyFlagsButDesiredEmpty = pseudoPropertyFlagsActive && _desiredProperties.isEmpty();
+    const bool returnNothingOnEmptyPropertyFlags = pseudoPropertyFlagsActive;
 
     if (_created == UNKNOWN_CREATED_TIME && !allowUnknownCreateTime) {
         // No entity properties can have been set so return without setting any default, zero property values.
         return properties;
     }
 
+    auto nodeList = DependencyManager::get<NodeList>();
+    bool isMyOwnAvatarEntity = _entityHostType == entity::HostType::AVATAR && (_owningAvatarID == AVATAR_SELF_ID || _owningAvatarID == Physics::getSessionUUID());
     if (_idSet && (!pseudoPropertyFlagsActive || pseudoPropertyFlags.test(EntityPseudoPropertyFlag::ID))) {
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER_ALWAYS(id, _id.toString());
     }
@@ -1722,7 +1726,7 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_RENDER_WITH_ZONES, renderWithZones);
     COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_BILLBOARD_MODE, billboardMode, getBillboardModeAsString());
     COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_TAGS, tags, getTagsAsVector());
-    _grab.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
+    _grab.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
     COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_MIRROR_MODE, mirrorMode, getMirrorModeAsString());
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PORTAL_EXIT_ID, portalExitID);
 
@@ -1743,7 +1747,7 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
     COPY_PROXY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_COLLISION_MASK, collisionMask, collidesWith, getCollisionMaskAsString());
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DYNAMIC, dynamic);
     COPY_PROXY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_DYNAMIC, dynamic, collisionsWillMove, getDynamic()); // legacy support
-    COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLLISION_SOUND_URL, collisionSoundURL);
+    COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_COLLISION_SOUND_URL, collisionSoundURL);
     COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ACTION_DATA, actionData);
 
     // Cloning
@@ -1769,10 +1773,10 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
     // Particles only
     if (_type == EntityTypes::ParticleEffect) {
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, getShapeTypeAsString());
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_COMPOUND_SHAPE_URL, compoundShapeURL);
         COPY_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_COLOR, color, u8vec3Color);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA, alpha);
-        _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
+        _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures);
 
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MAX_PARTICLES, maxParticles);
@@ -1829,11 +1833,11 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
     // Models only
     if (_type == EntityTypes::Model) {
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, getShapeTypeAsString());
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_COMPOUND_SHAPE_URL, compoundShapeURL);
         COPY_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_COLOR, color, u8vec3Color);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXTURES, textures);
 
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MODEL_URL, modelURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_MODEL_URL, modelURL);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MODEL_SCALE, modelScale);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_JOINT_ROTATIONS_SET, jointRotationsSet);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_JOINT_ROTATIONS, jointRotations);
@@ -1843,9 +1847,7 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GROUP_CULLED, groupCulled);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_BLENDSHAPE_COEFFICIENTS, blendshapeCoefficients);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USE_ORIGINAL_PIVOT, useOriginalPivot);
-        if (!pseudoPropertyFlagsButDesiredEmpty) {
-            _animation.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
-        }
+        _animation.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
     }
 
     // FIXME: Shouldn't provide a shapeType property for Box and Sphere entities.
@@ -1859,7 +1861,7 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
     if (_type == EntityTypes::Box || _type == EntityTypes::Sphere || _type == EntityTypes::Shape) {
         COPY_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_COLOR, color, u8vec3Color);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA, alpha);
-        _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
+        _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SHAPE, shape);
     }
 
@@ -1875,7 +1877,7 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
 
     // Text only
     if (_type == EntityTypes::Text) {
-        _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
+        _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
 
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_TEXT, text);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LINE_HEIGHT, lineHeight);
@@ -1898,20 +1900,18 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
     // Zones only
     if (_type == EntityTypes::Zone) {
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, getShapeTypeAsString());
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COMPOUND_SHAPE_URL, compoundShapeURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_COMPOUND_SHAPE_URL, compoundShapeURL);
 
-        if (!pseudoPropertyFlagsButDesiredEmpty) {
-            _keyLight.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
-            _ambientLight.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
-            _skybox.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
-            _haze.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
-            _bloom.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
-            _audio.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
-        }
+        _keyLight.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
+        _ambientLight.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
+        _skybox.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
+        _haze.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
+        _bloom.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
+        _audio.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
 
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FLYING_ALLOWED, flyingAllowed);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GHOSTING_ALLOWED, ghostingAllowed);
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FILTER_URL, filterURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_FILTER_URL, filterURL);
 
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_KEY_LIGHT_MODE, keyLightMode, getKeyLightModeAsString());
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_AMBIENT_LIGHT_MODE, ambientLightMode, getAmbientLightModeAsString());
@@ -1926,11 +1926,11 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
     if (_type == EntityTypes::Web) {
         COPY_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_COLOR, color, u8vec3Color);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA, alpha);
-        _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
+        _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
 
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SOURCE_URL, sourceUrl);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_SOURCE_URL, sourceUrl);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_DPI, dpi);
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SCRIPT_URL, scriptURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_SCRIPT_URL, scriptURL);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MAX_FPS, maxFPS);
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_INPUT_MODE, inputMode, getInputModeAsString());
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_WANTS_KEYBOARD_FOCUS, wantsKeyboardFocus);
@@ -1944,9 +1944,9 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VOXEL_VOLUME_SIZE, voxelVolumeSize);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VOXEL_DATA, voxelData);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_VOXEL_SURFACE_STYLE, voxelSurfaceStyle);
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_X_TEXTURE_URL, xTextureURL);
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_TEXTURE_URL, yTextureURL);
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Z_TEXTURE_URL, zTextureURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_X_TEXTURE_URL, xTextureURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_Y_TEXTURE_URL, yTextureURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_Z_TEXTURE_URL, zTextureURL);
 
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_X_N_NEIGHBOR_ID, xNNeighborID);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_Y_N_NEIGHBOR_ID, yNNeighborID);
@@ -1980,14 +1980,14 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
 
     // Materials
     if (_type == EntityTypes::Material) {
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MATERIAL_URL, materialURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_MATERIAL_URL, materialURL);
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_MATERIAL_MAPPING_MODE, materialMappingMode, getMaterialMappingModeAsString());
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MATERIAL_PRIORITY, priority);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PARENT_MATERIAL_NAME, parentMaterialName);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MATERIAL_MAPPING_POS, materialMappingPos);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MATERIAL_MAPPING_SCALE, materialMappingScale);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MATERIAL_MAPPING_ROT, materialMappingRot);
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MATERIAL_DATA, materialData);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_MATERIAL_DATA, materialData);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MATERIAL_REPEAT, materialRepeat);
     }
 
@@ -1995,15 +1995,16 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
     if (_type == EntityTypes::Image) {
         COPY_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_COLOR, color, u8vec3Color);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA, alpha);
-        _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
+        _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
 
-        COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_IMAGE_URL, imageURL);
+        COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_IMAGE_URL, imageURL);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EMISSIVE, emissive);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_KEEP_ASPECT_RATIO, keepAspectRatio);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SUB_IMAGE, subImage);
 
         // Handle conversions to old 'textures' property from "imageURL"
-        if (((!pseudoPropertyFlagsButDesiredEmpty && _desiredProperties.isEmpty()) || _desiredProperties.getHasProperty(PROP_IMAGE_URL)) &&
+        if ((isMyOwnAvatarEntity || nodeList->getThisNodeCanViewAssetURLs()) &&
+                ((!returnNothingOnEmptyPropertyFlags && _desiredProperties.isEmpty()) || _desiredProperties.getHasProperty(PROP_IMAGE_URL)) &&
                 (!skipDefaults || defaultEntityProperties._imageURL != _imageURL)) {
             ScriptValue textures = engine->newObject();
             textures.setProperty("tex.picture", _imageURL);
@@ -2015,7 +2016,7 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
     if (_type == EntityTypes::Grid) {
         COPY_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_COLOR, color, u8vec3Color);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ALPHA, alpha);
-        _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
+        _pulse.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
 
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GRID_FOLLOW_CAMERA, followCamera);
         COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MAJOR_GRID_EVERY, majorGridEvery);
@@ -2025,7 +2026,7 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s
     // Gizmo only
     if (_type == EntityTypes::Gizmo) {
         COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_GIZMO_TYPE, gizmoType, getGizmoTypeAsString());
-        _ring.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties);
+        _ring.copyToScriptValue(_desiredProperties, properties, engine, skipDefaults, defaultEntityProperties, returnNothingOnEmptyPropertyFlags, isMyOwnAvatarEntity);
     }
 
     /*@jsdoc
@@ -2700,7 +2701,7 @@ bool EntityItemProperties::entityPropertyFlagsFromScriptValue(const ScriptValue&
     if (object.isString()) {
         EntityPropertyInfo propertyInfo;
         if (getPropertyInfo(object.toString(), propertyInfo)) {
-            flags << propertyInfo.propertyEnum;
+            flags << propertyInfo.propertyEnums;
         }
     }
     else if (object.isArray()) {
@@ -2709,7 +2710,7 @@ bool EntityItemProperties::entityPropertyFlagsFromScriptValue(const ScriptValue&
             QString propertyName = object.property(i).toString();
             EntityPropertyInfo propertyInfo;
             if (getPropertyInfo(propertyName, propertyInfo)) {
-                flags << propertyInfo.propertyEnum;
+                flags << propertyInfo.propertyEnums;
             }
         }
     }
@@ -2960,7 +2961,7 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
         { // Keylight
             ADD_GROUP_PROPERTY_TO_MAP(PROP_KEYLIGHT_COLOR, KeyLight, keyLight, Color, color);
             ADD_GROUP_PROPERTY_TO_MAP(PROP_KEYLIGHT_INTENSITY, KeyLight, keyLight, Intensity, intensity);
-            ADD_GROUP_PROPERTY_TO_MAP(PROP_KEYLIGHT_DIRECTION, KeyLight, keylight, Direction, direction);
+            ADD_GROUP_PROPERTY_TO_MAP(PROP_KEYLIGHT_DIRECTION, KeyLight, keyLight, Direction, direction);
             ADD_GROUP_PROPERTY_TO_MAP(PROP_KEYLIGHT_CAST_SHADOW, KeyLight, keyLight, CastShadows, castShadows);
             ADD_GROUP_PROPERTY_TO_MAP_WITH_RANGE(PROP_KEYLIGHT_SHADOW_BIAS, KeyLight, keyLight, ShadowBias, shadowBias, 0.0f, 1.0f);
             ADD_GROUP_PROPERTY_TO_MAP_WITH_RANGE(PROP_KEYLIGHT_SHADOW_MAX_DISTANCE, KeyLight, keyLight, ShadowMaxDistance, shadowMaxDistance, 1.0f, 250.0f);
@@ -3117,14 +3118,14 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr
  */
 ScriptValue EntityPropertyInfoToScriptValue(ScriptEngine* engine, const EntityPropertyInfo& propertyInfo) {
     ScriptValue obj = engine->newObject();
-    obj.setProperty("propertyEnum", propertyInfo.propertyEnum);
+    obj.setProperty("propertyEnum", propertyInfo.propertyEnums.firstFlag());
     obj.setProperty("minimum", propertyInfo.minimum.toString());
     obj.setProperty("maximum", propertyInfo.maximum.toString());
     return obj;
 }
 
 bool EntityPropertyInfoFromScriptValue(const ScriptValue& object, EntityPropertyInfo& propertyInfo) {
-    propertyInfo.propertyEnum = (EntityPropertyList)object.property("propertyEnum").toVariant().toUInt();
+    propertyInfo.propertyEnums = (EntityPropertyList)object.property("propertyEnum").toVariant().toUInt();
     propertyInfo.minimum = object.property("minimum").toVariant();
     propertyInfo.maximum = object.property("maximum").toVariant();
     return true;
diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h
index 78e69f98b7..fde4ea5312 100644
--- a/libraries/entities/src/EntityItemProperties.h
+++ b/libraries/entities/src/EntityItemProperties.h
@@ -83,11 +83,11 @@ using u8vec3Color = glm::u8vec3;
 
 struct EntityPropertyInfo {
     EntityPropertyInfo(EntityPropertyList propEnum) :
-        propertyEnum(propEnum) {}
+        propertyEnums(propEnum) {}
     EntityPropertyInfo(EntityPropertyList propEnum, QVariant min, QVariant max) :
-        propertyEnum(propEnum), minimum(min), maximum(max) {}
+        propertyEnums(propEnum), minimum(min), maximum(max) {}
     EntityPropertyInfo() = default;
-    EntityPropertyList propertyEnum;
+    EntityPropertyFlags propertyEnums;
     QVariant minimum;
     QVariant maximum;
 };
diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h
index c9ecff41f2..ad9de6ac18 100644
--- a/libraries/entities/src/EntityItemPropertiesDefaults.h
+++ b/libraries/entities/src/EntityItemPropertiesDefaults.h
@@ -57,9 +57,9 @@ const glm::vec3 ENTITY_ITEM_DEFAULT_DIMENSIONS = glm::vec3(ENTITY_ITEM_DEFAULT_W
 const float ENTITY_ITEM_DEFAULT_VOLUME = ENTITY_ITEM_DEFAULT_WIDTH * ENTITY_ITEM_DEFAULT_WIDTH * ENTITY_ITEM_DEFAULT_WIDTH;
 const float ENTITY_ITEM_MIN_VOLUME = ENTITY_ITEM_MIN_DIMENSION * ENTITY_ITEM_MIN_DIMENSION * ENTITY_ITEM_MIN_DIMENSION;
 
-const float ENTITY_ITEM_MAX_DENSITY = 10000.0f; // kg/m^3 density of silver
-const float ENTITY_ITEM_MIN_DENSITY = 100.0f; // kg/m^3 density of balsa wood
-const float ENTITY_ITEM_DEFAULT_DENSITY = 1000.0f;   // density of water
+const float ENTITY_ITEM_MAX_DENSITY = 100000.0f; // kg/m^3 more than 5 times density of tungsten.
+const float ENTITY_ITEM_MIN_DENSITY = 0.1f; // kg/m^3 ten times less than air density.
+const float ENTITY_ITEM_DEFAULT_DENSITY = 1000.0f;   // density of water.
 const float ENTITY_ITEM_DEFAULT_MASS = ENTITY_ITEM_DEFAULT_DENSITY * ENTITY_ITEM_DEFAULT_VOLUME;
 
 const glm::vec3 ENTITY_ITEM_DEFAULT_VELOCITY = ENTITY_ITEM_ZERO_VEC3;
diff --git a/libraries/entities/src/EntityItemPropertiesMacros.h b/libraries/entities/src/EntityItemPropertiesMacros.h
index 3b6e424663..0fe438459d 100644
--- a/libraries/entities/src/EntityItemPropertiesMacros.h
+++ b/libraries/entities/src/EntityItemPropertiesMacros.h
@@ -139,7 +139,7 @@ inline ScriptValue convertScriptValue(ScriptEngine* e, const EntityItemID& v) {
 inline ScriptValue convertScriptValue(ScriptEngine* e, const AACube& v) { return aaCubeToScriptValue(e, v); }
 
 #define COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(X,G,g,P,p) \
-    if ((desiredProperties.isEmpty() || desiredProperties.getHasProperty(X)) && \
+    if (((!returnNothingOnEmptyPropertyFlags && desiredProperties.isEmpty()) || desiredProperties.getHasProperty(X)) && \
         (!skipDefaults || defaultEntityProperties.get##G().get##P() != get##P())) { \
         ScriptValue groupProperties = properties.property(#g); \
         if (!groupProperties.isValid()) { \
@@ -151,7 +151,7 @@ inline ScriptValue convertScriptValue(ScriptEngine* e, const AACube& v) { return
     }
 
 #define COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_TYPED(X,G,g,P,p,T) \
-    if ((desiredProperties.isEmpty() || desiredProperties.getHasProperty(X)) && \
+    if (((!returnNothingOnEmptyPropertyFlags && desiredProperties.isEmpty()) || desiredProperties.getHasProperty(X)) && \
         (!skipDefaults || defaultEntityProperties.get##G().get##P() != get##P())) { \
         ScriptValue groupProperties = properties.property(#g); \
         if (!groupProperties.isValid()) { \
@@ -163,7 +163,7 @@ inline ScriptValue convertScriptValue(ScriptEngine* e, const AACube& v) { return
     }
 
 #define COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_GETTER(X,G,g,P,p,M)                       \
-    if ((desiredProperties.isEmpty() || desiredProperties.getHasProperty(X)) &&       \
+    if (((!returnNothingOnEmptyPropertyFlags && desiredProperties.isEmpty()) || desiredProperties.getHasProperty(X)) &&       \
         (!skipDefaults || defaultEntityProperties.get##G().get##P() != get##P())) {   \
         ScriptValue groupProperties = properties.property(#g);                        \
         if (!groupProperties.isValid()) {                                             \
@@ -175,14 +175,14 @@ inline ScriptValue convertScriptValue(ScriptEngine* e, const AACube& v) { return
     }
 
 #define COPY_PROPERTY_TO_QSCRIPTVALUE(p,P) \
-    if (((!pseudoPropertyFlagsButDesiredEmpty && _desiredProperties.isEmpty()) || _desiredProperties.getHasProperty(p)) && \
+    if (((!returnNothingOnEmptyPropertyFlags && _desiredProperties.isEmpty()) || _desiredProperties.getHasProperty(p)) && \
         (!skipDefaults || defaultEntityProperties._##P != _##P)) { \
         ScriptValue V = convertScriptValue(engine, _##P); \
         properties.setProperty(#P, V);     \
     }
 
 #define COPY_PROPERTY_TO_QSCRIPTVALUE_TYPED(p,P,T) \
-    if ((_desiredProperties.isEmpty() || _desiredProperties.getHasProperty(p)) && \
+    if (((!returnNothingOnEmptyPropertyFlags && _desiredProperties.isEmpty()) || _desiredProperties.getHasProperty(p)) && \
         (!skipDefaults || defaultEntityProperties._##P != _##P)) { \
         ScriptValue V = T##_convertScriptValue(engine, _##P); \
         properties.setProperty(#P, V); \
@@ -192,7 +192,7 @@ inline ScriptValue convertScriptValue(ScriptEngine* e, const AACube& v) { return
     properties.setProperty(#P, G);
 
 #define COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(p, P, G) \
-    if (((!pseudoPropertyFlagsButDesiredEmpty && _desiredProperties.isEmpty()) || _desiredProperties.getHasProperty(p)) && \
+    if (((!returnNothingOnEmptyPropertyFlags && _desiredProperties.isEmpty()) || _desiredProperties.getHasProperty(p)) && \
         (!skipDefaults || defaultEntityProperties._##P != _##P)) { \
         ScriptValue V = convertScriptValue(engine, G); \
         properties.setProperty(#P, V); \
@@ -207,7 +207,7 @@ inline ScriptValue convertScriptValue(ScriptEngine* e, const AACube& v) { return
 
 // same as COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER but uses #X instead of #P in the setProperty() step
 #define COPY_PROXY_PROPERTY_TO_QSCRIPTVALUE_GETTER(p, P, X, G) \
-    if (((!pseudoPropertyFlagsButDesiredEmpty && _desiredProperties.isEmpty()) || _desiredProperties.getHasProperty(p)) && \
+    if (((!returnNothingOnEmptyPropertyFlags && _desiredProperties.isEmpty()) || _desiredProperties.getHasProperty(p)) && \
         (!skipDefaults || defaultEntityProperties._##P != _##P)) { \
         ScriptValue V = convertScriptValue(engine, G); \
         properties.setProperty(#X, V); \
@@ -219,6 +219,37 @@ inline ScriptValue convertScriptValue(ScriptEngine* e, const AACube& v) { return
         properties.setProperty(#P, V); \
     }
 
+#define COPY_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(p, P)                                                             \
+    if (((!returnNothingOnEmptyPropertyFlags && _desiredProperties.isEmpty()) || _desiredProperties.getHasProperty(p)) && \
+        (!skipDefaults || defaultEntityProperties._##P != _##P)) {                                                        \
+        if (isMyOwnAvatarEntity || nodeList->getThisNodeCanViewAssetURLs()) {                                             \
+            ScriptValue V = convertScriptValue(engine, _##P);                                                             \
+            properties.setProperty(#P, V);                                                                                \
+        } else {                                                                                                          \
+            const QString emptyURL = "";                                                                                  \
+            ScriptValue V = convertScriptValue(engine, emptyURL);                                                         \
+            properties.setProperty(#P, V);                                                                                \
+        }                                                                                                                 \
+    }
+
+#define COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(X, G, g, P, p)                                            \
+    if (((!returnNothingOnEmptyPropertyFlags && desiredProperties.isEmpty()) || desiredProperties.getHasProperty(X)) && \
+        (!skipDefaults || defaultEntityProperties.get##G().get##P() != get##P())) {                                     \
+        if (isMyOwnAvatarEntity || nodeList->getThisNodeCanViewAssetURLs()) {                                           \
+            ScriptValue groupProperties = properties.property(#g);                                                      \
+            if (!groupProperties.isValid()) {                                                                           \
+                groupProperties = engine->newObject();                                                                  \
+            }                                                                                                           \
+            ScriptValue V = convertScriptValue(engine, get##P());                                                       \
+            groupProperties.setProperty(#p, V);                                                                         \
+            properties.setProperty(#g, groupProperties);                                                                \
+        } else {                                                                                                        \
+            const QString emptyURL = "";                                                                                \
+            ScriptValue V = convertScriptValue(engine, emptyURL);                                                       \
+            properties.setProperty(#P, V);                                                                              \
+        }                                                                                                               \
+    }
+
 typedef QVector<glm::vec3> qVectorVec3;
 typedef QVector<glm::quat> qVectorQuat;
 typedef QVector<bool> qVectorBool;
@@ -466,14 +497,16 @@ inline QRect QRect_convertFromScriptValue(const ScriptValue& v, bool& isValid) {
     { \
         EntityPropertyInfo propertyInfo = EntityPropertyInfo(P); \
         _propertyInfos[#g "." #n] = propertyInfo; \
-		_enumsToPropertyStrings[P] = #g "." #n; \
+        _propertyInfos[#g].propertyEnums << P; \
+        _enumsToPropertyStrings[P] = #g "." #n; \
     }
 
 #define ADD_GROUP_PROPERTY_TO_MAP_WITH_RANGE(P, G, g, N, n, M, X) \
     { \
         EntityPropertyInfo propertyInfo = EntityPropertyInfo(P, M, X); \
         _propertyInfos[#g "." #n] = propertyInfo; \
-		_enumsToPropertyStrings[P] = #g "." #n; \
+        _propertyInfos[#g].propertyEnums << P; \
+        _enumsToPropertyStrings[P] = #g "." #n; \
     }
 
 #define DEFINE_CORE(N, n, T, V) \
diff --git a/libraries/entities/src/EntityScriptServerLogClient.cpp b/libraries/entities/src/EntityScriptServerLogClient.cpp
index 5d7d4017cd..7329cf1fdd 100644
--- a/libraries/entities/src/EntityScriptServerLogClient.cpp
+++ b/libraries/entities/src/EntityScriptServerLogClient.cpp
@@ -9,7 +9,11 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
 
+#include <QJsonDocument>
+#include <QJsonArray>
 #include "EntityScriptServerLogClient.h"
+#include "ScriptMessage.h"
+#include "ScriptEngines.h"
 
 EntityScriptServerLogClient::EntityScriptServerLogClient() {
     auto nodeList = DependencyManager::get<NodeList>();
@@ -35,9 +39,9 @@ void EntityScriptServerLogClient::disconnectNotify(const QMetaMethod& signal) {
 
 void EntityScriptServerLogClient::connectionsChanged() {
     auto numReceivers = receivers(SIGNAL(receivedNewLogLines(QString)));
-    if (!_subscribed && numReceivers > 0) {
+    if (!_subscribed && (numReceivers > 0 || _areMessagesRequestedByScripts)) {
         enableToEntityServerScriptLog(DependencyManager::get<NodeList>()->getThisNodeCanRez());
-    } else if (_subscribed && numReceivers == 0) {
+    } else if (_subscribed && (numReceivers == 0 && !_areMessagesRequestedByScripts)) {
         enableToEntityServerScriptLog(false);
     }
 }
@@ -62,7 +66,59 @@ void EntityScriptServerLogClient::enableToEntityServerScriptLog(bool enable) {
 }
 
 void EntityScriptServerLogClient::handleEntityServerScriptLogPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
-    emit receivedNewLogLines(QString::fromUtf8(message->readAll()));
+    QString messageText = QString::fromUtf8(message->readAll());
+    QJsonParseError error;
+    QJsonDocument document = QJsonDocument::fromJson(messageText.toUtf8(), &error);
+    emit receivedNewLogLines(messageText);
+    if(document.isNull()) {
+        qWarning() << "EntityScriptServerLogClient::handleEntityServerScriptLogPacket: Cannot parse JSON: " << error.errorString()
+            << " Contents: " << messageText;
+        return;
+    }
+    // Iterate through contents and emit messages
+    if(!document.isArray()) {
+        qWarning() << "EntityScriptServerLogClient::handleEntityServerScriptLogPacket: JSON is not an array: " << messageText;
+        return;
+    }
+
+    auto scriptEngines = DependencyManager::get<ScriptEngines>().data();
+
+    auto array = document.array();
+    for (int n = 0; n < array.size(); n++) {
+        if (!array[n].isObject()) {
+            qWarning() << "EntityScriptServerLogClient::handleEntityServerScriptLogPacket: message is not an object: " << messageText;
+            continue;
+        }
+        ScriptMessage scriptMessage;
+        if (!scriptMessage.fromJson(array[n].toObject())) {
+            qWarning() << "EntityScriptServerLogClient::handleEntityServerScriptLogPacket: message parsing failed: " << messageText;
+            continue;
+        }
+        switch (scriptMessage.getSeverity()) {
+            case ScriptMessage::Severity::SEVERITY_INFO:
+                emit scriptEngines->infoEntityMessage(scriptMessage.getMessage(), scriptMessage.getFileName(),
+                                                      scriptMessage.getLineNumber(), scriptMessage.getEntityID(), true);
+            break;
+
+            case ScriptMessage::Severity::SEVERITY_PRINT:
+                emit scriptEngines->printedEntityMessage(scriptMessage.getMessage(), scriptMessage.getFileName(),
+                                                  scriptMessage.getLineNumber(), scriptMessage.getEntityID(), true);
+            break;
+
+            case ScriptMessage::Severity::SEVERITY_WARNING:
+                emit scriptEngines->warningEntityMessage(scriptMessage.getMessage(), scriptMessage.getFileName(),
+                                                  scriptMessage.getLineNumber(), scriptMessage.getEntityID(), true);
+            break;
+
+            case ScriptMessage::Severity::SEVERITY_ERROR:
+                emit scriptEngines->errorEntityMessage(scriptMessage.getMessage(), scriptMessage.getFileName(),
+                                                  scriptMessage.getLineNumber(), scriptMessage.getEntityID(), true);
+            break;
+
+            default:
+            break;
+        }
+    }
 }
 
 void EntityScriptServerLogClient::nodeActivated(SharedNodePointer activatedNode) {
@@ -84,3 +140,8 @@ void EntityScriptServerLogClient::canRezChanged(bool canRez) {
         enableToEntityServerScriptLog(canRez);
     }
 }
+
+void EntityScriptServerLogClient::requestMessagesForScriptEngines(bool areMessagesRequested) {
+    _areMessagesRequestedByScripts = areMessagesRequested;
+    connectionsChanged();
+}
diff --git a/libraries/entities/src/EntityScriptServerLogClient.h b/libraries/entities/src/EntityScriptServerLogClient.h
index 9eee5daed8..e388de917a 100644
--- a/libraries/entities/src/EntityScriptServerLogClient.h
+++ b/libraries/entities/src/EntityScriptServerLogClient.h
@@ -33,6 +33,12 @@ class EntityScriptServerLogClient : public QObject, public Dependency {
 public:
     EntityScriptServerLogClient();
 
+    /**
+     * @brief This is called by ScriptEngines when scripts need access to entity server script messages and when access
+     * is not needed anymore.
+     */
+    void requestMessagesForScriptEngines(bool areMessagesRequested);
+
 signals:
 
     /*@jsdoc
@@ -66,7 +72,10 @@ private slots:
     void connectionsChanged();
 
 private:
+    std::atomic<bool> _areMessagesRequestedByScripts {false};
     bool _subscribed { false };
+
+    friend class ScriptEngines;
 };
 
 #endif // hifi_EntityScriptServerLogClient_h
diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp
index 4acf9b8d1b..523c95641e 100644
--- a/libraries/entities/src/EntityScriptingInterface.cpp
+++ b/libraries/entities/src/EntityScriptingInterface.cpp
@@ -97,6 +97,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership
     connect(nodeList.data(), &NodeList::canWriteAssetsChanged, this, &EntityScriptingInterface::canWriteAssetsChanged);
     connect(nodeList.data(), &NodeList::canGetAndSetPrivateUserDataChanged, this, &EntityScriptingInterface::canGetAndSetPrivateUserDataChanged);
     connect(nodeList.data(), &NodeList::canRezAvatarEntitiesChanged, this, &EntityScriptingInterface::canRezAvatarEntitiesChanged);
+    connect(nodeList.data(), &NodeList::canViewAssetURLsChanged, this, &EntityScriptingInterface::canViewAssetURLsChanged);
 
     auto& packetReceiver = nodeList->getPacketReceiver();
     packetReceiver.registerListener(PacketType::EntityScriptCallMethod,
@@ -291,6 +292,11 @@ bool EntityScriptingInterface::canRezAvatarEntities() {
     return nodeList->getThisNodeCanRezAvatarEntities();
 }
 
+bool EntityScriptingInterface::canViewAssetURLs() {
+    auto nodeList = DependencyManager::get<NodeList>();
+    return nodeList->getThisNodeCanViewAssetURLs();
+}
+
 void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) {
     if (_entityTree) {
         disconnect(_entityTree.get(), &EntityTree::addingEntityPointer, this, &EntityScriptingInterface::onAddingEntity);
@@ -785,7 +791,7 @@ QUuid EntityScriptingInterface::cloneEntity(const QUuid& entityIDToClone) {
 
 EntityItemProperties EntityScriptingInterface::getEntityProperties(const QUuid& entityID) {
     const EntityPropertyFlags noSpecificProperties;
-    return getEntityPropertiesInternal(entityID, noSpecificProperties);
+    return getEntityPropertiesInternal(entityID, noSpecificProperties, false);
 }
 
 ScriptValue EntityScriptingInterface::getEntityProperties(const QUuid& entityID, const ScriptValue& extendedDesiredProperties) {
@@ -810,12 +816,14 @@ ScriptValue EntityScriptingInterface::getEntityProperties(const QUuid& entityID,
         }
     }
 
-    EntityItemProperties properties = getEntityPropertiesInternal(entityID, desiredProperties);
+    EntityItemProperties properties = getEntityPropertiesInternal(entityID, desiredProperties, !desiredPseudoPropertyFlags.none());
 
     return properties.copyToScriptValue(extendedDesiredProperties.engine().get(), false, false, false, desiredPseudoPropertyFlags);
 }
 
-EntityItemProperties EntityScriptingInterface::getEntityPropertiesInternal(const QUuid& entityID, EntityPropertyFlags desiredProperties) {
+EntityItemProperties EntityScriptingInterface::getEntityPropertiesInternal(const QUuid& entityID,
+                                                                           EntityPropertyFlags desiredProperties,
+                                                                           bool returnNothingOnEmptyPropertyFlags) {
 
     PROFILE_RANGE(script_entities, __FUNCTION__);
 
@@ -838,7 +846,7 @@ EntityItemProperties EntityScriptingInterface::getEntityPropertiesInternal(const
                     desiredProperties.setHasProperty(PROP_PARENT_JOINT_INDEX);
                 }
 
-                if (desiredProperties.isEmpty()) {
+                if (desiredProperties.isEmpty() && !returnNothingOnEmptyPropertyFlags) {
                     // these are left out of EntityItem::getEntityProperties so that localPosition and localRotation
                     // don't end up in json saves, etc.  We still want them here, though.
                     EncodeBitstreamParams params; // unknown
@@ -850,7 +858,7 @@ EntityItemProperties EntityScriptingInterface::getEntityPropertiesInternal(const
                     desiredProperties.setHasProperty(PROP_LOCAL_DIMENSIONS);
                 }
 
-                results = entity->getProperties(desiredProperties);
+                results = entity->getProperties(desiredProperties, true);
             }
         });
     }
diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h
index c677bdf0a1..6fcb863fe7 100644
--- a/libraries/entities/src/EntityScriptingInterface.h
+++ b/libraries/entities/src/EntityScriptingInterface.h
@@ -206,7 +206,8 @@ public:
      * @param {Uuid[]} entityIDs - The IDs of the entities to get the properties of.
      * @param {string[]|string} [desiredProperties=[]] - The name or names of the properties to get. For properties that are 
      *     objects (e.g., the <code>"keyLight"</code> property), use the property and subproperty names in dot notation (e.g., 
-     *     <code>"keyLight.color"</code>).
+     *     <code>"keyLight.color"</code>). Getting all subproperties with the name of an object is currently not supported (e.g.,
+     *     passing the <code>"keyLight"</code> property only).
      * @returns {Entities.EntityProperties[]} The specified properties of each entity for each entity that can be found. If 
      *     none of the entities can be found, then an empty array is returned. If no properties are specified, then all 
      *     properties are returned.
@@ -287,6 +288,14 @@ public slots:
      */
     Q_INVOKABLE bool canRezAvatarEntities();
 
+    /*@jsdoc
+     * Checks whether or not the script can view asset URLs
+     * @function Entities.canViewAssetURLs
+     * @returns {boolean} <code>true</code> if the domain server will allow the script to view asset URLs,
+     *     otherwise <code>false</code>.
+     */
+    Q_INVOKABLE bool canViewAssetURLs();
+
     /*@jsdoc
      * <p>How an entity is hosted and sent to others for display.</p>
      * <table>
@@ -395,7 +404,24 @@ public slots:
      */
     Q_INVOKABLE EntityItemProperties getEntityProperties(const QUuid& entityID);
     Q_INVOKABLE ScriptValue getEntityProperties(const QUuid& entityID, const ScriptValue &desiredProperties);
-    Q_INVOKABLE EntityItemProperties getEntityPropertiesInternal(const QUuid& entityID, EntityPropertyFlags desiwredProperties);
+    /**
+     * @brief Internal function to get entity properties.
+     *
+     * It's being called by EntityScriptingInterface::getEntityProperties
+     * and also from C++ side in several places in the source code.
+     *
+     * @param entityID The ID of the entity to get the properties of.
+     * @param desiredProperties Flags representing requested entity properties
+     * @param returnNothingOnEmptyPropertyFlags If this parameter is false and property flags are empty, then all possible
+     * properties will get returned. This is needed because we divide properties requested through getEntityProperties into
+     * real properties and pseudo properties. Only real properties are passed here as desiredProperties, so if user requests
+     * only pseudo properties, then desiredProperties will be empty. In such case we need to pass true
+     * as returnNothingOnEmptyPropertyFlags to avoid mistakenly returning all the properties.
+     * @return EntityItemProperties Requested properties of the entity if it can be found.
+     */
+
+    Q_INVOKABLE EntityItemProperties getEntityPropertiesInternal(const QUuid& entityID, EntityPropertyFlags desiredProperties,
+                                                                 bool returnNothingOnEmptyPropertyFlags);
     //Q_INVOKABLE EntityItemProperties getEntityProperties(const QUuid& entityID, EntityPropertyFlags desiredProperties);
 
     /*@jsdoc
@@ -2250,6 +2276,14 @@ signals:
      */
     void canRezAvatarEntitiesChanged(bool canRezAvatarEntities);
 
+    /*@jsdoc
+     * Triggered when your ability to view asset URLs is changed.
+     * @function Entities.canViewAssetURLsChanged
+     * @param {boolean} canViewAssetURLs - <code>true</code> if the script can view asset URLs,
+     *     <code>false</code> if it can't.
+     * @returns {Signal}
+     */
+    void canViewAssetURLsChanged(bool canViewAssetURLs);
 
     /*@jsdoc
      * Triggered when a mouse button is clicked while the mouse cursor is on an entity, or a controller trigger is fully 
diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp
index f8ae726b90..8f2321dbb3 100644
--- a/libraries/entities/src/EntityTree.cpp
+++ b/libraries/entities/src/EntityTree.cpp
@@ -2589,11 +2589,10 @@ bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer
     }
     entityDescription["DataVersion"] = _persistDataVersion;
     entityDescription["Id"] = _persistID;
-    const std::lock_guard<std::mutex> scriptLock(scriptEngineMutex);
-    RecurseOctreeToMapOperator theOperator(entityDescription, element, scriptEngine.get(), skipDefaultValues,
-                                            skipThoseWithBadParents, _myAvatar);
-    withReadLock([&] {
-        recurseTreeWithOperator(&theOperator);
+    _helperScriptEngine.run( [&] {
+        RecurseOctreeToMapOperator theOperator(entityDescription, element, _helperScriptEngine.get(), skipDefaultValues,
+                                               skipThoseWithBadParents, _myAvatar);
+        withReadLock([&] { recurseTreeWithOperator(&theOperator); });
     });
     return true;
 }
@@ -2764,11 +2763,10 @@ bool EntityTree::readFromMap(QVariantMap& map, const bool isImport) {
         }
 
         EntityItemProperties properties;
-        {
-            const std::lock_guard<std::mutex> scriptLock(scriptEngineMutex);
-            ScriptValue entityScriptValue = variantMapToScriptValue(entityMap, *scriptEngine);
+        _helperScriptEngine.run( [&] {
+            ScriptValue entityScriptValue = variantMapToScriptValue(entityMap, *_helperScriptEngine.get());
             EntityItemPropertiesFromScriptValueIgnoreReadOnly(entityScriptValue, properties);
-        }
+        });
 
         EntityItemID entityItemID;
         if (entityMap.contains("id")) {
@@ -2922,13 +2920,12 @@ bool EntityTree::readFromMap(QVariantMap& map, const bool isImport) {
 }
 
 bool EntityTree::writeToJSON(QString& jsonString, const OctreeElementPointer& element) {
-    const std::lock_guard<std::mutex> scriptLock(scriptEngineMutex);
-    RecurseOctreeToJSONOperator theOperator(element, scriptEngine.get(), jsonString);
-    withReadLock([&] {
-        recurseTreeWithOperator(&theOperator);
-    });
+    _helperScriptEngine.run( [&] {
+        RecurseOctreeToJSONOperator theOperator(element, _helperScriptEngine.get(), jsonString);
+        withReadLock([&] { recurseTreeWithOperator(&theOperator); });
 
-    jsonString = theOperator.getJson();
+        jsonString = theOperator.getJson();
+    });
     return true;
 }
 
diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h
index 1161bec6e9..1d43a91ec8 100644
--- a/libraries/entities/src/EntityTree.h
+++ b/libraries/entities/src/EntityTree.h
@@ -15,6 +15,7 @@
 #include <QSet>
 #include <QVector>
 
+#include <HelperScriptEngine.h>
 #include <Octree.h>
 #include <SpatialParentFinder.h>
 
@@ -388,8 +389,7 @@ private:
                                          MovingEntitiesOperator& moveOperator, bool force, bool tellServer);
 
     // Script engine for writing entity tree data to and from JSON
-    std::mutex scriptEngineMutex;
-    ScriptEnginePointer scriptEngine{ newScriptEngine() };
+    HelperScriptEngine _helperScriptEngine;
 };
 
 void convertGrabUserDataToProperties(EntityItemProperties& properties);
diff --git a/libraries/entities/src/GrabPropertyGroup.cpp b/libraries/entities/src/GrabPropertyGroup.cpp
index f0026d8904..a4037ff98f 100644
--- a/libraries/entities/src/GrabPropertyGroup.cpp
+++ b/libraries/entities/src/GrabPropertyGroup.cpp
@@ -20,7 +20,9 @@
 
 void GrabPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
                                           ScriptEngine* engine, bool skipDefaults,
-                                          EntityItemProperties& defaultEntityProperties) const {
+                                          EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                          bool isMyOwnAvatarEntity) const {
+    auto nodeList = DependencyManager::get<NodeList>();
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_GRAB_GRABBABLE, Grab, grab, Grabbable, grabbable);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_GRAB_KINEMATIC, Grab, grab, GrabKinematic, grabKinematic);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_GRAB_FOLLOWS_CONTROLLER, Grab, grab, GrabFollowsController, grabFollowsController);
@@ -36,7 +38,7 @@ void GrabPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProp
                                         EquippableRightPosition, equippableRightPosition);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_GRAB_RIGHT_EQUIPPABLE_ROTATION_OFFSET, Grab, grab,
                                         EquippableRightRotation, equippableRightRotation);
-    COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_GRAB_EQUIPPABLE_INDICATOR_URL, Grab, grab,
+    COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_GRAB_EQUIPPABLE_INDICATOR_URL, Grab, grab,
                                         EquippableIndicatorURL, equippableIndicatorURL);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_GRAB_EQUIPPABLE_INDICATOR_SCALE, Grab, grab,
                                         EquippableIndicatorScale, equippableIndicatorScale);
diff --git a/libraries/entities/src/GrabPropertyGroup.h b/libraries/entities/src/GrabPropertyGroup.h
index 368867a6d6..23211bde21 100644
--- a/libraries/entities/src/GrabPropertyGroup.h
+++ b/libraries/entities/src/GrabPropertyGroup.h
@@ -75,7 +75,8 @@ public:
     // EntityItemProperty related helpers
     virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
                                    ScriptEngine* engine, bool skipDefaults,
-                                   EntityItemProperties& defaultEntityProperties) const override;
+                                   EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                   bool isMyOwnAvatarEntity) const override;
     virtual void copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) override;
 
     void merge(const GrabPropertyGroup& other);
diff --git a/libraries/entities/src/HazePropertyGroup.cpp b/libraries/entities/src/HazePropertyGroup.cpp
index 7cd41e8d59..fd091de8ac 100644
--- a/libraries/entities/src/HazePropertyGroup.cpp
+++ b/libraries/entities/src/HazePropertyGroup.cpp
@@ -18,7 +18,9 @@
 #include "EntityItemProperties.h"
 #include "EntityItemPropertiesMacros.h"
 
-void HazePropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, ScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const {
+void HazePropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, ScriptEngine* engine,
+                                          bool skipDefaults, EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                          bool isMyOwnAvatarEntity) const {
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_HAZE_RANGE, Haze, haze, HazeRange, hazeRange);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_HAZE_COLOR, Haze, haze, HazeColor, hazeColor, u8vec3Color);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_HAZE_GLARE_COLOR, Haze, haze, HazeGlareColor, hazeGlareColor, u8vec3Color);
diff --git a/libraries/entities/src/HazePropertyGroup.h b/libraries/entities/src/HazePropertyGroup.h
index 2b899871fa..a84ec20713 100644
--- a/libraries/entities/src/HazePropertyGroup.h
+++ b/libraries/entities/src/HazePropertyGroup.h
@@ -80,7 +80,8 @@ public:
     // EntityItemProperty related helpers
     virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
                                    ScriptEngine* engine, bool skipDefaults,
-                                   EntityItemProperties& defaultEntityProperties) const override;
+                                   EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                   bool isMyOwnAvatarEntity) const override;
     virtual void copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) override;
 
     void merge(const HazePropertyGroup& other);
diff --git a/libraries/entities/src/KeyLightPropertyGroup.cpp b/libraries/entities/src/KeyLightPropertyGroup.cpp
index 74ea8dc520..f431aa55cc 100644
--- a/libraries/entities/src/KeyLightPropertyGroup.cpp
+++ b/libraries/entities/src/KeyLightPropertyGroup.cpp
@@ -28,7 +28,8 @@ const float KeyLightPropertyGroup::DEFAULT_KEYLIGHT_SHADOW_BIAS { 0.5f };
 const float KeyLightPropertyGroup::DEFAULT_KEYLIGHT_SHADOW_MAX_DISTANCE { 40.0f };
 
 void KeyLightPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, 
-    ScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const {
+    ScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+    bool isMyOwnAvatarEntity) const {
         
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_KEYLIGHT_COLOR, KeyLight, keyLight, Color, color, u8vec3Color);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_KEYLIGHT_INTENSITY, KeyLight, keyLight, Intensity, intensity);
diff --git a/libraries/entities/src/KeyLightPropertyGroup.h b/libraries/entities/src/KeyLightPropertyGroup.h
index 7d92813a54..4a412f9802 100644
--- a/libraries/entities/src/KeyLightPropertyGroup.h
+++ b/libraries/entities/src/KeyLightPropertyGroup.h
@@ -50,7 +50,8 @@ public:
     // EntityItemProperty related helpers
     virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
                                    ScriptEngine* engine, bool skipDefaults,
-                                   EntityItemProperties& defaultEntityProperties) const override;
+                                   EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                   bool isMyOwnAvatarEntity) const override;
     virtual void copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) override;
 
     void merge(const KeyLightPropertyGroup& other);
diff --git a/libraries/entities/src/PropertyGroup.h b/libraries/entities/src/PropertyGroup.h
index fab78c5ef6..b73c2dad2a 100644
--- a/libraries/entities/src/PropertyGroup.h
+++ b/libraries/entities/src/PropertyGroup.h
@@ -34,7 +34,8 @@ public:
     virtual ~PropertyGroup() = default;
 
     // EntityItemProperty related helpers
-    virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, ScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const = 0;
+    virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, ScriptEngine* engine, bool skipDefaults,
+        EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags, bool isMyOwnAvatarEntity) const = 0;
     virtual void copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) = 0;
     virtual void debugDump() const { }
     virtual void listChangedProperties(QList<QString>& out) { }
diff --git a/libraries/entities/src/PulsePropertyGroup.cpp b/libraries/entities/src/PulsePropertyGroup.cpp
index b760ac3c29..ab61a1f8ad 100644
--- a/libraries/entities/src/PulsePropertyGroup.cpp
+++ b/libraries/entities/src/PulsePropertyGroup.cpp
@@ -60,8 +60,9 @@ void PulsePropertyGroup::setAlphaModeFromString(const QString& pulseMode) {
 }
 
 void PulsePropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
-                                          ScriptEngine* engine, bool skipDefaults,
-                                          EntityItemProperties& defaultEntityProperties) const {
+                                           ScriptEngine* engine, bool skipDefaults,
+                                           EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                           bool isMyOwnAvatarEntity) const {
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_PULSE_MIN, Pulse, pulse, Min, min);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_PULSE_MAX, Pulse, pulse, Max, max);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_PULSE_PERIOD, Pulse, pulse, Period, period);
diff --git a/libraries/entities/src/PulsePropertyGroup.h b/libraries/entities/src/PulsePropertyGroup.h
index e874f114e4..649005b970 100644
--- a/libraries/entities/src/PulsePropertyGroup.h
+++ b/libraries/entities/src/PulsePropertyGroup.h
@@ -44,7 +44,8 @@ public:
     // EntityItemProperty related helpers
     virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
                                    ScriptEngine* engine, bool skipDefaults,
-                                   EntityItemProperties& defaultEntityProperties) const override;
+                                   EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                   bool isMyOwnAvatarEntity) const override;
     virtual void copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) override;
 
     void merge(const PulsePropertyGroup& other);
diff --git a/libraries/entities/src/RingGizmoPropertyGroup.cpp b/libraries/entities/src/RingGizmoPropertyGroup.cpp
index f8e106c722..68021f44a2 100644
--- a/libraries/entities/src/RingGizmoPropertyGroup.cpp
+++ b/libraries/entities/src/RingGizmoPropertyGroup.cpp
@@ -23,8 +23,9 @@ const float RingGizmoPropertyGroup::MIN_RADIUS = 0.0f;
 const float RingGizmoPropertyGroup::MAX_RADIUS = 0.5f;
 
 void RingGizmoPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
-                                          ScriptEngine* engine, bool skipDefaults,
-                                          EntityItemProperties& defaultEntityProperties) const {
+                                               ScriptEngine* engine, bool skipDefaults,
+                                               EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                               bool isMyOwnAvatarEntity) const {
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_START_ANGLE, Ring, ring, StartAngle, startAngle);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_END_ANGLE, Ring, ring, EndAngle, endAngle);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_INNER_RADIUS, Ring, ring, InnerRadius, innerRadius);
diff --git a/libraries/entities/src/RingGizmoPropertyGroup.h b/libraries/entities/src/RingGizmoPropertyGroup.h
index 7779fea3c1..51ef709f5b 100644
--- a/libraries/entities/src/RingGizmoPropertyGroup.h
+++ b/libraries/entities/src/RingGizmoPropertyGroup.h
@@ -60,7 +60,8 @@ public:
     // EntityItemProperty related helpers
     virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
                                    ScriptEngine* engine, bool skipDefaults,
-                                   EntityItemProperties& defaultEntityProperties) const override;
+                                   EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                   bool isMyOwnAvatarEntity) const override;
     virtual void copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) override;
 
     void merge(const RingGizmoPropertyGroup& other);
diff --git a/libraries/entities/src/SkyboxPropertyGroup.cpp b/libraries/entities/src/SkyboxPropertyGroup.cpp
index 12be5fe4df..9c3ad46fce 100644
--- a/libraries/entities/src/SkyboxPropertyGroup.cpp
+++ b/libraries/entities/src/SkyboxPropertyGroup.cpp
@@ -20,9 +20,11 @@
 
 const glm::u8vec3 SkyboxPropertyGroup::DEFAULT_COLOR = { 0, 0, 0 };
 
-void SkyboxPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, ScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const {
+void SkyboxPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, ScriptEngine* engine,
+        bool skipDefaults, EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags, bool isMyOwnAvatarEntity) const {
+    auto nodeList = DependencyManager::get<NodeList>();
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_TYPED(PROP_SKYBOX_COLOR, Skybox, skybox, Color, color, u8vec3Color);
-    COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_SKYBOX_URL, Skybox, skybox, URL, url);
+    COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE_IF_URL_PERMISSION(PROP_SKYBOX_URL, Skybox, skybox, URL, url);
 }
 
 void SkyboxPropertyGroup::copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) {
diff --git a/libraries/entities/src/SkyboxPropertyGroup.h b/libraries/entities/src/SkyboxPropertyGroup.h
index 0cb7d8568b..30c9ef1d3a 100644
--- a/libraries/entities/src/SkyboxPropertyGroup.h
+++ b/libraries/entities/src/SkyboxPropertyGroup.h
@@ -43,7 +43,8 @@ public:
     // EntityItemProperty related helpers
     virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
                                    ScriptEngine* engine, bool skipDefaults,
-                                   EntityItemProperties& defaultEntityProperties) const override;
+                                   EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                   bool isMyOwnAvatarEntity) const override;
     virtual void copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) override;
 
     void merge(const SkyboxPropertyGroup& other);
diff --git a/libraries/entities/src/ZoneAudioPropertyGroup.cpp b/libraries/entities/src/ZoneAudioPropertyGroup.cpp
index aeedeea977..34bde41e1d 100644
--- a/libraries/entities/src/ZoneAudioPropertyGroup.cpp
+++ b/libraries/entities/src/ZoneAudioPropertyGroup.cpp
@@ -17,7 +17,8 @@
 #include "EntityItemProperties.h"
 #include "EntityItemPropertiesMacros.h"
 
-void ZoneAudioPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, ScriptEngine* engine, bool skipDefaults, EntityItemProperties& defaultEntityProperties) const {
+void ZoneAudioPropertyGroup::copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties, ScriptEngine* engine, bool skipDefaults,
+                                               EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags, bool isMyOwnAvatarEntity) const {
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_REVERB_ENABLED, Audio, audio, ReverbEnabled, reverbEnabled);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_REVERB_TIME, Audio, audio, ReverbTime, reverbTime);
     COPY_GROUP_PROPERTY_TO_QSCRIPTVALUE(PROP_REVERB_WET_LEVEL, Audio, audio, ReverbWetLevel, reverbWetLevel);
diff --git a/libraries/entities/src/ZoneAudioPropertyGroup.h b/libraries/entities/src/ZoneAudioPropertyGroup.h
index a99a43e2be..bbe32b1549 100644
--- a/libraries/entities/src/ZoneAudioPropertyGroup.h
+++ b/libraries/entities/src/ZoneAudioPropertyGroup.h
@@ -44,7 +44,8 @@ public:
     // EntityItemProperty related helpers
     virtual void copyToScriptValue(const EntityPropertyFlags& desiredProperties, ScriptValue& properties,
                                    ScriptEngine* engine, bool skipDefaults,
-                                   EntityItemProperties& defaultEntityProperties) const override;
+                                   EntityItemProperties& defaultEntityProperties, bool returnNothingOnEmptyPropertyFlags,
+                                   bool isMyOwnAvatarEntity) const override;
     virtual void copyFromScriptValue(const ScriptValue& object, const QSet<QString> &namesSet, bool& _defaultSettings) override;
 
     void merge(const ZoneAudioPropertyGroup& other);
diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp
index c0116274ee..86b0df982a 100644
--- a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp
+++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.cpp
@@ -101,8 +101,6 @@ GLBackend::CommandCall GLBackend::_commandCalls[Batch::NUM_COMMANDS] =
     (&::gpu::gl::GLBackend::do_glUniformMatrix3fv),
     (&::gpu::gl::GLBackend::do_glUniformMatrix4fv),
 
-    (&::gpu::gl::GLBackend::do_glColor4f),
-
     (&::gpu::gl::GLBackend::do_pushProfileRange),
     (&::gpu::gl::GLBackend::do_popProfileRange),
 };
@@ -851,22 +849,6 @@ void GLBackend::do_glUniformMatrix4fv(const Batch& batch, size_t paramOffset) {
     (void)CHECK_GL_ERROR();
 }
 
-void GLBackend::do_glColor4f(const Batch& batch, size_t paramOffset) {
-
-    glm::vec4 newColor(
-        batch._params[paramOffset + 3]._float,
-        batch._params[paramOffset + 2]._float,
-        batch._params[paramOffset + 1]._float,
-        batch._params[paramOffset + 0]._float);
-
-    if (_input._colorAttribute != newColor) {
-        _input._colorAttribute = newColor;
-        glVertexAttrib4fv(gpu::Stream::COLOR, &_input._colorAttribute.r);
-        _input._hasColorAttribute = true;
-    }
-    (void)CHECK_GL_ERROR();
-}
-
 void GLBackend::releaseBuffer(GLuint id, Size size) const {
     Lock lock(_trashMutex);
     _currentFrameTrash.buffersTrash.push_back({ id, size });
diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h
index 5545858877..8a1648a01b 100644
--- a/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h
+++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackend.h
@@ -242,8 +242,6 @@ public:
     virtual void do_glUniformMatrix3fv(const Batch& batch, size_t paramOffset) final;
     virtual void do_glUniformMatrix4fv(const Batch& batch, size_t paramOffset) final;
 
-    virtual void do_glColor4f(const Batch& batch, size_t paramOffset) final;
-
     // The State setters called by the GLState::Commands when a new state is assigned
     virtual void do_setStateFillMode(int32 mode) final;
     virtual void do_setStateCullMode(int32 mode) final;
@@ -351,8 +349,6 @@ protected:
     struct InputStageState {
         bool _invalidFormat { true };
         bool _lastUpdateStereoState { false };
-        bool _hasColorAttribute { false };
-        bool _hadColorAttribute { false };
         FormatReference _format { GPU_REFERENCE_INIT_VALUE };
         std::string _formatKey;
 
@@ -369,8 +365,6 @@ protected:
         std::array<Offset, MAX_NUM_INPUT_BUFFERS> _bufferStrides;
         std::array<GLuint, MAX_NUM_INPUT_BUFFERS> _bufferVBOs;
 
-        glm::vec4 _colorAttribute { 1.0f };
-
         BufferReference _indexBuffer;
         Offset _indexBufferOffset { 0 };
         Type _indexBufferType { UINT32 };
diff --git a/libraries/gpu-gl-common/src/gpu/gl/GLBackendInput.cpp b/libraries/gpu-gl-common/src/gpu/gl/GLBackendInput.cpp
index 4efd5f9941..e5b0ead027 100644
--- a/libraries/gpu-gl-common/src/gpu/gl/GLBackendInput.cpp
+++ b/libraries/gpu-gl-common/src/gpu/gl/GLBackendInput.cpp
@@ -103,9 +103,6 @@ void GLBackend::resetInputStage() {
     reset(_input._format);
     _input._formatKey.clear();
     _input._invalidFormat = false;
-    _input._hasColorAttribute = false;
-    _input._hadColorAttribute = false;
-    _input._colorAttribute = vec4(1.0f);
     _input._attributeActivation.reset();
 
     for (uint32_t i = 0; i < _input._buffers.size(); i++) {
@@ -163,8 +160,6 @@ void GLBackend::updateInput() {
 #endif
     _input._lastUpdateStereoState = isStereoNow;
 
-    bool hasColorAttribute = _input._hasColorAttribute;
-
     if (_input._invalidFormat) {
         InputStageState::ActivationCache newActivation;
 
@@ -194,8 +189,6 @@ void GLBackend::updateInput() {
 
                     GLenum perLocationSize = attrib._element.getLocationSize();
 
-                    hasColorAttribute |= slot == Stream::COLOR;
-
                     for (GLuint locNum = 0; locNum < locationCount; ++locNum) {
                         GLuint attriNum = (GLuint)(slot + locNum);
                         newActivation.set(attriNum);
@@ -226,12 +219,6 @@ void GLBackend::updateInput() {
                 glVertexBindingDivisor(bufferChannelNum, frequency);
 #endif
             }
-
-            if (!hasColorAttribute && _input._hadColorAttribute) {
-                // The previous input stage had a color attribute but this one doesn't, so reset the color to pure white.
-                _input._colorAttribute = glm::vec4(1.0f);
-                glVertexAttrib4fv(Stream::COLOR, &_input._colorAttribute.r);
-            }
         }
 
         // Manage Activation what was and what is expected now
@@ -253,9 +240,6 @@ void GLBackend::updateInput() {
         _stats._ISNumFormatChanges++;
     }
 
-    _input._hadColorAttribute = hasColorAttribute;
-    _input._hasColorAttribute = false;
-
     if (_input._invalidBuffers.any()) {
         auto vbo = _input._bufferVBOs.data();
         auto offset = _input._bufferOffsets.data();
diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp
index 188b4a1084..bdb37f6319 100644
--- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp
+++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendInput.cpp
@@ -33,8 +33,6 @@ void GL41Backend::updateInput() {
 #endif
     _input._lastUpdateStereoState = isStereoNow;
 
-    bool hasColorAttribute = _input._hasColorAttribute;
-
     if (_input._invalidFormat || _input._invalidBuffers.any()) {
 
         auto format = acquire(_input._format);
@@ -110,8 +108,6 @@ void GL41Backend::updateInput() {
                             uintptr_t pointer = (uintptr_t)(attrib._offset + offsets[bufferNum]);
                             GLboolean isNormalized = attrib._element.isNormalized();
 
-                            hasColorAttribute |= slot == Stream::COLOR;
-
                             for (size_t locNum = 0; locNum < locationCount; ++locNum) {
                                 if (attrib._element.isInteger()) {
                                     glVertexAttribIPointer(slot + (GLuint)locNum, count, type, stride,
@@ -131,17 +127,8 @@ void GL41Backend::updateInput() {
                     }
                 }
             }
-
-            if (!hasColorAttribute && _input._hadColorAttribute) {
-                // The previous input stage had a color attribute but this one doesn't, so reset the color to pure white.
-                _input._colorAttribute = glm::vec4(1.0f);
-                glVertexAttrib4fv(Stream::COLOR, &_input._colorAttribute.r);
-            }
         }
         // everything format related should be in sync now
         _input._invalidFormat = false;
     }
-
-    _input._hadColorAttribute = hasColorAttribute;
-    _input._hasColorAttribute = false;
 }
diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp
index 385ddca065..d65e8b0af9 100644
--- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp
+++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.cpp
@@ -12,7 +12,9 @@
 #include <list>
 #include <functional>
 #include <glm/gtc/type_ptr.hpp>
+#include <QLoggingCategory>
 
+#include "glad/glad.h"
 Q_LOGGING_CATEGORY(gpugl45logging, "hifi.gpu.gl45")
 
 using namespace gpu;
@@ -21,9 +23,27 @@ using namespace gpu::gl45;
 GLint GL45Backend::MAX_COMBINED_SHADER_STORAGE_BLOCKS{ 0 };
 GLint GL45Backend::MAX_UNIFORM_LOCATIONS{ 0 };
 
+#ifdef GLAD_DEBUG
+static void post_call_callback_gl(const char *name, void *funcptr, int len_args, ...) {
+    (void)funcptr;
+    (void)len_args;
+
+    GLenum error_code = glad_glGetError();
+    if (error_code != GL_NO_ERROR) {
+        qCWarning(gpugl45logging) << "OpenGL error" << error_code << "in" << name;
+    }
+}
+#endif
+
+
 static void staticInit() {
     static std::once_flag once;
     std::call_once(once, [&] {
+#ifdef GLAD_DEBUG
+        // This sets the post call callback to a logging function. By default it prints on
+        // stderr and skips our log. It only exists in debug builds.
+        glad_set_post_callback(&post_call_callback_gl);
+#endif
         glGetIntegerv(GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, &GL45Backend::MAX_COMBINED_SHADER_STORAGE_BLOCKS);
         glGetIntegerv(GL_MAX_UNIFORM_LOCATIONS, &GL45Backend::MAX_UNIFORM_LOCATIONS);
     });
@@ -82,7 +102,7 @@ void GL45Backend::do_drawIndexed(const Batch& batch, size_t paramOffset) {
     uint32 startIndex = batch._params[paramOffset + 0]._uint;
 
     GLenum glType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType];
-    
+
     auto typeByteSize = TYPE_SIZE[_input._indexBufferType];
     GLvoid* indexBufferByteOffset = reinterpret_cast<GLvoid*>(startIndex * typeByteSize + _input._indexBufferOffset);
 
@@ -148,7 +168,7 @@ void GL45Backend::do_drawIndexedInstanced(const Batch& batch, size_t paramOffset
     GLenum glType = gl::ELEMENT_TYPE_TO_GL[_input._indexBufferType];
     auto typeByteSize = TYPE_SIZE[_input._indexBufferType];
     GLvoid* indexBufferByteOffset = reinterpret_cast<GLvoid*>(startIndex * typeByteSize + _input._indexBufferOffset);
- 
+
     if (isStereo()) {
         GLint trueNumInstances = 2 * numInstances;
 
diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp
index 8add4a9296..7513ff7caf 100644
--- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp
+++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendInput.cpp
@@ -35,8 +35,6 @@ void GL45Backend::updateInput() {
 #endif
     _input._lastUpdateStereoState = isStereoNow;
 
-    bool hasColorAttribute = _input._hasColorAttribute;
-
     if (_input._invalidFormat) {
         InputStageState::ActivationCache newActivation;
 
@@ -66,8 +64,6 @@ void GL45Backend::updateInput() {
 
                     GLenum perLocationSize = attrib._element.getLocationSize();
 
-                    hasColorAttribute |= slot == Stream::COLOR;
-
                     for (GLuint locNum = 0; locNum < locationCount; ++locNum) {
                         GLuint attriNum = (GLuint)(slot + locNum);
                         newActivation.set(attriNum);
@@ -98,12 +94,6 @@ void GL45Backend::updateInput() {
                 glVertexBindingDivisor(bufferChannelNum, frequency);
 #endif
             }
-
-            if (!hasColorAttribute && _input._hadColorAttribute) {
-                // The previous input stage had a color attribute but this one doesn't, so reset the color to pure white.
-                _input._colorAttribute = glm::vec4(1.0f);
-                glVertexAttrib4fv(Stream::COLOR, &_input._colorAttribute.r);
-            }
         }
 
         // Manage Activation what was and what is expected now
@@ -125,9 +115,6 @@ void GL45Backend::updateInput() {
         _stats._ISNumFormatChanges++;
     }
 
-    _input._hadColorAttribute = hasColorAttribute;
-    _input._hasColorAttribute = false;
-
     if (_input._invalidBuffers.any()) {
         auto vbo = _input._bufferVBOs.data();
         auto offset = _input._bufferOffsets.data();
diff --git a/libraries/gpu/src/gpu/Batch.cpp b/libraries/gpu/src/gpu/Batch.cpp
index a41f586d74..bd4aef9768 100644
--- a/libraries/gpu/src/gpu/Batch.cpp
+++ b/libraries/gpu/src/gpu/Batch.cpp
@@ -693,15 +693,6 @@ void Batch::_glUniformMatrix4fv(int32 location, int count, uint8 transpose, cons
     _params.emplace_back(location);
 }
 
-void Batch::_glColor4f(float red, float green, float blue, float alpha) {
-    ADD_COMMAND(glColor4f);
-
-    _params.emplace_back(alpha);
-    _params.emplace_back(blue);
-    _params.emplace_back(green);
-    _params.emplace_back(red);
-}
-
 void Batch::finishFrame(BufferUpdates& updates) {
     PROFILE_RANGE(render_gpu, __FUNCTION__);
 
diff --git a/libraries/gpu/src/gpu/Batch.h b/libraries/gpu/src/gpu/Batch.h
index f89dd3ea90..3cf8184913 100644
--- a/libraries/gpu/src/gpu/Batch.h
+++ b/libraries/gpu/src/gpu/Batch.h
@@ -30,11 +30,11 @@ class QDebug;
 #define BATCH_PREALLOCATE_MIN 128
 namespace gpu {
 
-// The named batch data provides a mechanism for accumulating data into buffers over the course 
-// of many independent calls.  For instance, two objects in the scene might both want to render 
+// The named batch data provides a mechanism for accumulating data into buffers over the course
+// of many independent calls.  For instance, two objects in the scene might both want to render
 // a simple box, but are otherwise unaware of each other.  The common code that they call to render
-// the box can create buffers to store the rendering parameters for each box and register a function 
-// that will be called with the accumulated buffer data when the batch commands are finally 
+// the box can create buffers to store the rendering parameters for each box and register a function
+// that will be called with the accumulated buffer data when the batch commands are finally
 // executed against the backend
 
 
@@ -100,15 +100,15 @@ public:
     void clear();
 
     // Batches may need to override the context level stereo settings
-    // if they're performing framebuffer copy operations, like the 
+    // if they're performing framebuffer copy operations, like the
     // deferred lighting resolution mechanism
     void enableStereo(bool enable = true);
     bool isStereoEnabled() const;
 
-    // Stereo batches will pre-translate the view matrix, but this isn't 
-    // appropriate for skyboxes or other things intended to be drawn at 
-    // infinite distance, so provide a mechanism to render in stereo 
-    // without the pre-translation of the view.  
+    // Stereo batches will pre-translate the view matrix, but this isn't
+    // appropriate for skyboxes or other things intended to be drawn at
+    // infinite distance, so provide a mechanism to render in stereo
+    // without the pre-translation of the view.
     void enableSkybox(bool enable = true);
     bool isSkyboxEnabled() const;
 
@@ -147,7 +147,7 @@ public:
     // Indirect buffer is used by the multiDrawXXXIndirect calls
     // The indirect buffer contains the command descriptions to execute multiple drawcalls in a single call
     void setIndirectBuffer(const BufferPointer& buffer, Offset offset = 0, Offset stride = 0);
-    
+
     // multi command desctription for multiDrawIndexedIndirect
     class DrawIndirectCommand {
     public:
@@ -249,7 +249,7 @@ public:
     void popProfileRange();
 
     // TODO: As long as we have gl calls explicitely issued from interface
-    // code, we need to be able to record and batch these calls. THe long 
+    // code, we need to be able to record and batch these calls. THe long
     // term strategy is to get rid of any GL calls in favor of the HIFI GPU API
     // For now, instead of calling the raw gl Call, use the equivalent call on the batch so the call is beeing recorded
     // THe implementation of these functions is in GLBackend.cpp
@@ -288,8 +288,6 @@ public:
         _glUniformMatrix3fv(location, 1, false, glm::value_ptr(v));
     }
 
-    void _glColor4f(float red, float green, float blue, float alpha);
-
     // Maybe useful but shoudln't be public. Please convince me otherwise
     // Well porting to gles i need it...
     void runLambda(std::function<void()> f);
@@ -352,7 +350,7 @@ public:
         COMMAND_stopNamedCall,
 
         // TODO: As long as we have gl calls explicitely issued from interface
-        // code, we need to be able to record and batch these calls. THe long 
+        // code, we need to be able to record and batch these calls. THe long
         // term strategy is to get rid of any GL calls in favor of the HIFI GPU API
         COMMAND_glUniform1i,
         COMMAND_glUniform1f,
@@ -365,8 +363,6 @@ public:
         COMMAND_glUniformMatrix3fv,
         COMMAND_glUniformMatrix4fv,
 
-        COMMAND_glColor4f,
-
         COMMAND_pushProfileRange,
         COMMAND_popProfileRange,
 
@@ -383,7 +379,7 @@ public:
         union {
 #if (QT_POINTER_SIZE == 8)
             size_t _size;
-#endif            
+#endif
             int32 _int;
             uint32 _uint;
             float _float;
@@ -391,7 +387,7 @@ public:
         };
 #if (QT_POINTER_SIZE == 8)
         Param(size_t val) : _size(val) {}
-#endif            
+#endif
         Param(int32 val) : _int(val) {}
         Param(uint32 val) : _uint(val) {}
         Param(float val) : _float(val) {}
@@ -408,7 +404,7 @@ public:
     public:
         typedef T Data;
         Data _data;
-        Cache<T>(const Data& data) : _data(data) {}
+        Cache(const Data& data) : _data(data) {}
         static size_t _max;
 
         class Vector {
@@ -575,7 +571,7 @@ private:
 
 #else
 
-#define PROFILE_RANGE_BATCH(batch, name) 
+#define PROFILE_RANGE_BATCH(batch, name)
 
 #endif
 
diff --git a/libraries/gpu/src/gpu/Buffer.h b/libraries/gpu/src/gpu/Buffer.h
index 49eed80b54..a30c6474c1 100644
--- a/libraries/gpu/src/gpu/Buffer.h
+++ b/libraries/gpu/src/gpu/Buffer.h
@@ -60,7 +60,7 @@ public:
     Size getNumTypedElements() const { return getSize() / sizeof(T); };
 
     const Byte* getData() const { return getSysmem().readData(); }
-    
+
     // Resize the buffer
     // Keep previous data [0 to min(pSize, mSize)]
     Size resize(Size pSize);
@@ -95,7 +95,7 @@ public:
     // \return the number of bytes copied
     Size append(Size size, const Byte* data);
 
-    template <typename T> 
+    template <typename T>
     Size append(const T& t) {
         return append(sizeof(t), reinterpret_cast<const Byte*>(&t));
     }
@@ -110,10 +110,10 @@ public:
 
 
     const GPUObjectPointer gpuObject {};
-    
+
     // Access the sysmem object, limited to ourselves and GPUObject derived classes
     const Sysmem& getSysmem() const { return _sysmem; }
-    
+
     bool isDirty() const {
         return _pages(PageManager::DIRTY);
     }
@@ -127,7 +127,7 @@ protected:
     // For use by the render thread to avoid the intermediate step of getUpdate/applyUpdate
     void flush() const;
 
-    // FIXME don't maintain a second buffer continuously.  We should be able to apply updates 
+    // FIXME don't maintain a second buffer continuously.  We should be able to apply updates
     // directly to the GL object and discard _renderSysmem and _renderPages
     mutable PageManager _renderPages;
     mutable Sysmem _renderSysmem;
@@ -292,7 +292,7 @@ public:
     // Direct memory access to the buffer contents is incompatible with the paging memory scheme
     template <typename T> Iterator<T> begin() { return Iterator<T>(&edit<T>(0), _stride); }
     template <typename T> Iterator<T> end() { return Iterator<T>(&edit<T>(getNum<T>()), _stride); }
-#else 
+#else
     template <typename T> Iterator<const T> begin() const { return Iterator<const T>(&get<T>(), _stride); }
     template <typename T> Iterator<const T> end() const {
         // reimplement get<T> without bounds checking
@@ -378,7 +378,7 @@ public:
         return *(reinterpret_cast<T*> (_buffer->editData() + elementOffset));
     }
 };
- 
+
 
     template <class T> class StructBuffer : public gpu::BufferView {
     public:
@@ -387,8 +387,8 @@ public:
             U t;
             return std::make_shared<gpu::Buffer>(sizeof(U), (const gpu::Byte*) &t, sizeof(U));
         }
-        ~StructBuffer<T>() {};
-        StructBuffer<T>() : gpu::BufferView(makeBuffer<T>()) {}
+        ~StructBuffer() {};
+        StructBuffer() : gpu::BufferView(makeBuffer<T>()) {}
 
 
         T& edit() {
diff --git a/libraries/gpu/src/gpu/FrameIOKeys.h b/libraries/gpu/src/gpu/FrameIOKeys.h
index 2d88158afb..5a5cfdf2b1 100644
--- a/libraries/gpu/src/gpu/FrameIOKeys.h
+++ b/libraries/gpu/src/gpu/FrameIOKeys.h
@@ -202,8 +202,6 @@ constexpr const char*  COMMAND_NAMES[] = {
     "glUniformMatrix3fv",
     "glUniformMatrix4fv",
 
-    "glColor4f",
-
     "pushProfileRange",
     "popProfileRange",
 };
diff --git a/libraries/gpu/src/gpu/State.h b/libraries/gpu/src/gpu/State.h
index 86bc5d4c5a..757169a138 100644
--- a/libraries/gpu/src/gpu/State.h
+++ b/libraries/gpu/src/gpu/State.h
@@ -41,6 +41,7 @@ class GPUObject;
 class State {
 public:
     State();
+    State(const State& state) : _values(state._values), _signature(state._signature), _stamp(state._stamp) {}
     virtual ~State();
 
     Stamp getStamp() const { return _stamp; }
@@ -464,7 +465,6 @@ public:
     const GPUObjectPointer gpuObject{};
 
 protected:
-    State(const State& state);
     State& operator=(const State& state);
 
     Data _values;
diff --git a/libraries/graphics-scripting/CMakeLists.txt b/libraries/graphics-scripting/CMakeLists.txt
index 3542704fc8..46dc2130ea 100644
--- a/libraries/graphics-scripting/CMakeLists.txt
+++ b/libraries/graphics-scripting/CMakeLists.txt
@@ -2,3 +2,7 @@ set(TARGET_NAME graphics-scripting)
 setup_hifi_library()
 link_hifi_libraries(shared networking graphics model-serializers image material-networking model-networking script-engine)
 include_hifi_library_headers(gpu)
+
+if (WIN32)
+  add_compile_definitions(_USE_MATH_DEFINES)
+endif()
\ No newline at end of file
diff --git a/libraries/graphics-scripting/src/graphics-scripting/Forward.h b/libraries/graphics-scripting/src/graphics-scripting/Forward.h
index 847937ba4f..23a7426610 100644
--- a/libraries/graphics-scripting/src/graphics-scripting/Forward.h
+++ b/libraries/graphics-scripting/src/graphics-scripting/Forward.h
@@ -52,35 +52,55 @@ namespace scriptable {
         ScriptableMaterial(const ScriptableMaterial& material) { *this = material; }
         ScriptableMaterial& operator=(const ScriptableMaterial& material);
 
-        QString name;
-        QString model;
-        float opacity;
-        float roughness;
-        float metallic;
-        float scattering;
-        bool unlit;
-        glm::vec3 emissive;
-        glm::vec3 albedo;
-        QString emissiveMap;
-        QString albedoMap;
-        QString opacityMap;
-        QString opacityMapMode;
-        float opacityCutoff;
-        QString metallicMap;
-        QString specularMap;
-        QString roughnessMap;
-        QString glossMap;
-        QString normalMap;
-        QString bumpMap;
-        QString occlusionMap;
-        QString lightMap;
-        QString scatteringMap;
+        QString name { "" };
+        QString model { "" };
+        float opacity { 0.0f };
+        float roughness { 0.0f };
+        float metallic { 0.0f };
+        float scattering { 0.0f };
+        bool unlit { false };
+        glm::vec3 emissive { 0.0f };
+        glm::vec3 albedo { 0.0f };
+        QString emissiveMap { "" };
+        QString albedoMap { "" };
+        QString opacityMap { "" };
+        QString opacityMapMode { "" };
+        float opacityCutoff { 0.0f };
+        QString metallicMap { "" };
+        QString specularMap { "" };
+        QString roughnessMap { "" };
+        QString glossMap { "" };
+        QString normalMap { "" };
+        QString bumpMap { "" };
+        QString occlusionMap { "" };
+        QString lightMap { "" };
+        QString scatteringMap { "" };
         std::array<glm::mat4, graphics::Material::NUM_TEXCOORD_TRANSFORMS> texCoordTransforms;
-        QString cullFaceMode;
-        bool defaultFallthrough;
+        QString cullFaceMode { "" };
+        bool defaultFallthrough { false };
         std::unordered_map<uint, bool> propertyFallthroughs; // not actually exposed to script
 
-        QString procedural;
+        QString procedural { "" };
+
+        glm::vec3 shade { 0.0f };
+        QString shadeMap { "" };
+        float shadingShift { 0.0f };
+        QString shadingShiftMap { "" };
+        float shadingToony { 0.0f };
+        glm::vec3 matcap { 0.0f };
+        QString matcapMap { "" };
+        glm::vec3 parametricRim { 0.0f };
+        float parametricRimFresnelPower { 0.0f };
+        float parametricRimLift { 0.0f };
+        QString rimMap { "" };
+        float rimLightingMix { 0.0f };
+        QString outlineWidthMode { "" };
+        float outlineWidth { 0.0f };
+        glm::vec3 outline { 0.0f };
+        QString uvAnimationMaskMap { "" };
+        float uvAnimationScrollXSpeed { 0.0f };
+        float uvAnimationScrollYSpeed { 0.0f };
+        float uvAnimationRotationSpeed { 0.0f };
 
         graphics::MaterialKey key { 0 };
     };
@@ -88,7 +108,7 @@ namespace scriptable {
     /*@jsdoc
      * A material layer.
      * @typedef {object} Graphics.MaterialLayer
-     * @property {Graphics.Material} material - The layer's material.
+     * @property {Entities.Material} material - The layer's material.
      * @property {number} priority - The priority of the layer. If multiple materials are applied to a mesh part, only the 
      *     layer with the highest priority is applied, with materials of the same priority randomly assigned.
      */
diff --git a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp
index 0dd5b95532..d907b5e9d6 100644
--- a/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp
+++ b/libraries/graphics-scripting/src/graphics-scripting/GraphicsScriptingInterface.cpp
@@ -369,120 +369,6 @@ namespace scriptable {
         return true;
     }
 
-    /*@jsdoc
-     * A material in a {@link GraphicsModel}.
-     * @typedef {object} Graphics.Material
-     * @property {string} name - The name of the material.
-     * @property {string} model - Different material models support different properties and rendering modes. Supported models 
-     *     are: <code>"hifi_pbr"</code> and <code>"hifi_shader_simple"</code>.
-     * @property {Vec3|string} [albedo] - The albedo color. Component values are in the range <code>0.0</code> &ndash;
-     *     <code>1.0</code>.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     * @property {number|string} [opacity] - The opacity, range <code>0.0</code> &ndash; <code>1.0</code>.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *
-     * @property {number|string} [opacityCutoff] - The opacity cutoff threshold used to determine the opaque texels of the
-     *     <code>opacityMap</code> when <code>opacityMapMode</code> is <code>"OPACITY_MAP_MASK"</code>. Range <code>0.0</code>
-     *     &ndash; <code>1.0</code>.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {number|string} [roughness] - The roughness, range <code>0.0</code> &ndash; <code>1.0</code>.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {number|string} [metallic] - The metallicness, range <code>0.0</code> &ndash; <code>1.0</code>.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {number|string} [scattering] - The scattering, range <code>0.0</code> &ndash; <code>1.0</code>.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {boolean|string} [unlit] - <code>true</code> if the material is unaffected by lighting, <code>false</code> if it
-     *     it is lit by the key light and local lights.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {Vec3|string} [emissive] - The emissive color, i.e., the color that the material emits. Component values are
-     *     in the range <code>0.0</code> &ndash; <code>1.0</code>.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [albedoMap] - The URL of the albedo texture image.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [opacityMap] - The URL of the opacity texture image.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [opacityMapMode] - The mode defining the interpretation of the opacity map. Values can be:
-     *     <ul>
-     *         <li><code>"OPACITY_MAP_OPAQUE"</code> for ignoring the opacity map information.</li>
-     *         <li><code>"OPACITY_MAP_MASK"</code> for using the <code>opacityMap</code> as a mask, where only the texel greater
-     *         than <code>opacityCutoff</code> are visible and rendered opaque.</li>
-     *         <li><code>"OPACITY_MAP_BLEND"</code> for using the <code>opacityMap</code> for alpha blending the material surface
-     *         with the background.</li>
-     *     </ul>
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [occlusionMap] - The URL of the occlusion texture image.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [lightMap] - The URL of the light map texture image.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [lightmapParams] - Parameters for controlling how <code>lightMap</code> is used.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     *     <p><em>Currently not used.</em></p>
-     * @property {string} [scatteringMap] - The URL of the scattering texture image.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [emissiveMap] - The URL of the emissive texture image.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [metallicMap] - The URL of the metallic texture image.
-     *     If <code>"fallthrough"</code> then it and <code>specularMap</code> fall through to the material below.
-     *     Only use one of <code>metallicMap</code> and <code>specularMap</code>.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [specularMap] - The URL of the specular texture image.
-     *     Only use one of <code>metallicMap</code> and <code>specularMap</code>.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [roughnessMap] - The URL of the roughness texture image.
-     *     If <code>"fallthrough"</code> then it and <code>glossMap</code> fall through to the material below.
-     *     Only use one of <code>roughnessMap</code> and <code>glossMap</code>.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [glossMap] - The URL of the gloss texture image.
-     *     Only use one of <code>roughnessMap</code> and <code>glossMap</code>.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [normalMap] - The URL of the normal texture image.
-     *     If <code>"fallthrough"</code> then it and <code>bumpMap</code> fall through to the material below.
-     *     Only use one of <code>normalMap</code> and <code>bumpMap</code>.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [bumpMap] - The URL of the bump texture image.
-     *     Only use one of <code>normalMap</code> and <code>bumpMap</code>.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {string} [materialParams] - Parameters for controlling the material projection and repetition.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     *     <p><em>Currently not used.</em></p>
-     * @property {string} [cullFaceMode="CULL_BACK"] - Specifies Which faces of the geometry to render. Values can be:
-     *     <ul>
-     *         <li><code>"CULL_NONE"</code> to render both sides of the geometry.</li>
-     *         <li><code>"CULL_FRONT"</code> to cull the front faces of the geometry.</li>
-     *         <li><code>"CULL_BACK"</code> (the default) to cull the back faces of the geometry.</li>
-     *     </ul>
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {Mat4|string} [texCoordTransform0] - The transform to use for all of the maps apart from
-     *     <code>occlusionMap</code> and <code>lightMap</code>.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     * @property {Mat4|string} [texCoordTransform1] - The transform to use for <code>occlusionMap</code> and
-     *     <code>lightMap</code>.
-     *     If <code>"fallthrough"</code> then it falls through to the material below.
-     *     <code>"hifi_pbr"</code> model only.
-     *
-     * @property {string} procedural - The definition of a procedural shader material.
-     *     <code>"hifi_shader_simple"</code> model only.
-     *     <p><em>Currently not used.</em></p>
-     *
-     * @property {boolean} defaultFallthrough - <code>true</code> if all properties fall through to the material below unless 
-     *     they are set, <code>false</code> if properties respect their individual fall-through settings.
-     */
     ScriptValue scriptableMaterialToScriptValue(ScriptEngine* engine, const scriptable::ScriptableMaterial &material) {
         ScriptValue obj = engine->newObject();
         obj.setProperty("name", material.name);
@@ -503,7 +389,7 @@ namespace scriptable {
             obj.setProperty("albedo", vec3ColorToScriptValue(engine, material.albedo));
         }
 
-        if (material.model.toStdString() == graphics::Material::HIFI_PBR) {
+        if (material.model.toStdString() == graphics::Material::HIFI_PBR || material.model.toStdString() == graphics::Material::VRM_MTOON) {
             if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_CUTOFF_VAL_BIT)) {
                 obj.setProperty("opacityCutoff", FALLTHROUGH);
             } else if (material.key.isOpacityCutoff()) {
@@ -516,30 +402,6 @@ namespace scriptable {
                 obj.setProperty("opacityMapMode", material.opacityMapMode);
             }
 
-            if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT)) {
-                obj.setProperty("roughness", FALLTHROUGH);
-            } else if (material.key.isGlossy()) {
-                obj.setProperty("roughness", material.roughness);
-            }
-
-            if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT)) {
-                obj.setProperty("metallic", FALLTHROUGH);
-            } else if (material.key.isMetallic()) {
-                obj.setProperty("metallic", material.metallic);
-            }
-
-            if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT)) {
-                obj.setProperty("scattering", FALLTHROUGH);
-            } else if (material.key.isScattering()) {
-                obj.setProperty("scattering", material.scattering);
-            }
-
-            if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT)) {
-                obj.setProperty("unlit", FALLTHROUGH);
-            } else if (material.key.isUnlit()) {
-                obj.setProperty("unlit", material.unlit);
-            }
-
             if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT)) {
                 obj.setProperty("emissive", FALLTHROUGH);
             } else if (material.key.isEmissive()) {
@@ -562,41 +424,6 @@ namespace scriptable {
                 obj.setProperty("opacityMap", material.opacityMap);
             }
 
-            if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT)) {
-                obj.setProperty("occlusionMap", FALLTHROUGH);
-            } else if (!material.occlusionMap.isEmpty()) {
-                obj.setProperty("occlusionMap", material.occlusionMap);
-            }
-
-            if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::LIGHT_MAP_BIT)) {
-                obj.setProperty("lightMap", FALLTHROUGH);
-            } else if (!material.lightMap.isEmpty()) {
-                obj.setProperty("lightMap", material.lightMap);
-            }
-
-            if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT)) {
-                obj.setProperty("scatteringMap", FALLTHROUGH);
-            } else if (!material.scatteringMap.isEmpty()) {
-                obj.setProperty("scatteringMap", material.scatteringMap);
-            }
-
-            // Only set one of each of these
-            if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) {
-                obj.setProperty("metallicMap", FALLTHROUGH);
-            } else if (!material.metallicMap.isEmpty()) {
-                obj.setProperty("metallicMap", material.metallicMap);
-            } else if (!material.specularMap.isEmpty()) {
-                obj.setProperty("specularMap", material.specularMap);
-            }
-
-            if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) {
-                obj.setProperty("roughnessMap", FALLTHROUGH);
-            } else if (!material.roughnessMap.isEmpty()) {
-                obj.setProperty("roughnessMap", material.roughnessMap);
-            } else if (!material.glossMap.isEmpty()) {
-                obj.setProperty("glossMap", material.glossMap);
-            }
-
             if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::NORMAL_MAP_BIT)) {
                 obj.setProperty("normalMap", FALLTHROUGH);
             } else if (!material.normalMap.isEmpty()) {
@@ -616,10 +443,7 @@ namespace scriptable {
                 obj.setProperty("texCoordTransform1", mat4toScriptValue(engine, material.texCoordTransforms[1]));
             }
 
-            // These need to be implemented, but set the fallthrough for now
-            if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) {
-                obj.setProperty("lightmapParams", FALLTHROUGH);
-            }
+            // This needs to be implemented, but set the fallthrough for now
             if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::MATERIAL_PARAMS)) {
                 obj.setProperty("materialParams", FALLTHROUGH);
             }
@@ -629,6 +453,196 @@ namespace scriptable {
             } else if (!material.cullFaceMode.isEmpty()) {
                 obj.setProperty("cullFaceMode", material.cullFaceMode);
             }
+
+            if (material.model.toStdString() == graphics::Material::HIFI_PBR) {
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT)) {
+                    obj.setProperty("roughness", FALLTHROUGH);
+                } else if (material.key.isGlossy()) {
+                    obj.setProperty("roughness", material.roughness);
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT)) {
+                    obj.setProperty("metallic", FALLTHROUGH);
+                } else if (material.key.isMetallic()) {
+                    obj.setProperty("metallic", material.metallic);
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT)) {
+                    obj.setProperty("scattering", FALLTHROUGH);
+                } else if (material.key.isScattering()) {
+                    obj.setProperty("scattering", material.scattering);
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT)) {
+                    obj.setProperty("unlit", FALLTHROUGH);
+                } else if (material.key.isUnlit()) {
+                    obj.setProperty("unlit", material.unlit);
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT)) {
+                    obj.setProperty("occlusionMap", FALLTHROUGH);
+                } else if (!material.occlusionMap.isEmpty()) {
+                    obj.setProperty("occlusionMap", material.occlusionMap);
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::LIGHT_MAP_BIT)) {
+                    obj.setProperty("lightMap", FALLTHROUGH);
+                } else if (!material.lightMap.isEmpty()) {
+                    obj.setProperty("lightMap", material.lightMap);
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT)) {
+                    obj.setProperty("scatteringMap", FALLTHROUGH);
+                } else if (!material.scatteringMap.isEmpty()) {
+                    obj.setProperty("scatteringMap", material.scatteringMap);
+                }
+
+                // Only set one of each of these
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) {
+                    obj.setProperty("metallicMap", FALLTHROUGH);
+                } else if (!material.metallicMap.isEmpty()) {
+                    obj.setProperty("metallicMap", material.metallicMap);
+                } else if (!material.specularMap.isEmpty()) {
+                    obj.setProperty("specularMap", material.specularMap);
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) {
+                    obj.setProperty("roughnessMap", FALLTHROUGH);
+                } else if (!material.roughnessMap.isEmpty()) {
+                    obj.setProperty("roughnessMap", material.roughnessMap);
+                } else if (!material.glossMap.isEmpty()) {
+                    obj.setProperty("glossMap", material.glossMap);
+                }
+
+                // This needs to be implemented, but set the fallthrough for now
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) {
+                    obj.setProperty("lightmapParams", FALLTHROUGH);
+                }
+            } else {
+                // See the mappings in ProceduralMatericalCache.h
+                // SHADE_VAL_BIT = graphics::MaterialKey::FlagBit::UNLIT_VAL_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT)) {
+                    obj.setProperty("shade", FALLTHROUGH);
+                } else if (material.key._flags[graphics::MaterialKey::UNLIT_VAL_BIT]) {
+                    obj.setProperty("shade", vec3ColorToScriptValue(engine, material.shade));
+                }
+
+                // SHADE_MAP_BIT = graphics::MaterialKey::FlagBit::ROUGHNESS_MAP_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) {
+                    obj.setProperty("shadeMap", FALLTHROUGH);
+                } else if (!material.shadeMap.isEmpty()) {
+                    obj.setProperty("shadeMap", material.shadeMap);
+                }
+
+                // SHADING_SHIFT_VAL_BIT = graphics::MaterialKey::FlagBit::METALLIC_VAL_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT)) {
+                    obj.setProperty("shadingShift", FALLTHROUGH);
+                } else if (material.key._flags[graphics::MaterialKey::METALLIC_VAL_BIT]) {
+                    obj.setProperty("shadingShift", material.shadingShift);
+                }
+
+                // SHADING_SHIFT_MAP_BIT = graphics::MaterialKey::FlagBit::METALLIC_MAP_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) {
+                    obj.setProperty("shadingShiftMap", FALLTHROUGH);
+                } else if (!material.shadingShiftMap.isEmpty()) {
+                    obj.setProperty("shadingShiftMap", material.shadingShiftMap);
+                }
+
+                // SHADING_TOONY_VAL_BIT = graphics::MaterialKey::FlagBit::GLOSSY_VAL_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT)) {
+                    obj.setProperty("shadingToony", FALLTHROUGH);
+                } else if (material.key._flags[graphics::MaterialKey::GLOSSY_VAL_BIT]) {
+                    obj.setProperty("shadingToony", material.shadingToony);
+                }
+
+                // MATCAP_VAL_BIT = graphics::MaterialKey::FlagBit::EXTRA_1_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EXTRA_1_BIT)) {
+                    obj.setProperty("matcap", FALLTHROUGH);
+                } else if (material.key._flags[graphics::MaterialKey::EXTRA_1_BIT]) {
+                    obj.setProperty("matcap", vec3ColorToScriptValue(engine, material.matcap));
+                }
+
+                // MATCAP_MAP_BIT = graphics::MaterialKey::FlagBit::OCCLUSION_MAP_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT)) {
+                    obj.setProperty("matcapMap", FALLTHROUGH);
+                } else if (!material.matcapMap.isEmpty()) {
+                    obj.setProperty("matcapMap", material.matcapMap);
+                }
+
+                // PARAMETRIC_RIM_VAL_BIT = graphics::MaterialKey::FlagBit::EXTRA_2_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EXTRA_2_BIT)) {
+                    obj.setProperty("parametricRim", FALLTHROUGH);
+                } else if (material.key._flags[graphics::MaterialKey::EXTRA_2_BIT]) {
+                    obj.setProperty("parametricRim", vec3ColorToScriptValue(engine, material.parametricRim));
+                }
+
+                // PARAMETRIC_RIM_POWER_VAL_BIT = graphics::MaterialKey::FlagBit::EXTRA_3_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EXTRA_3_BIT)) {
+                    obj.setProperty("parametricRimFresnelPower", FALLTHROUGH);
+                } else if (material.key._flags[graphics::MaterialKey::EXTRA_3_BIT]) {
+                    obj.setProperty("parametricRimFresnelPower", material.parametricRimFresnelPower);
+                }
+
+                // PARAMETRIC_RIM_LIFT_VAL_BIT = graphics::MaterialKey::FlagBit::EXTRA_4_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EXTRA_4_BIT)) {
+                    obj.setProperty("parametricRimLift", FALLTHROUGH);
+                } else if (material.key._flags[graphics::MaterialKey::EXTRA_4_BIT]) {
+                    obj.setProperty("parametricRimLift", material.parametricRimLift);
+                }
+
+                // RIM_MAP_BIT = graphics::MaterialKey::FlagBit::SCATTERING_MAP_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT)) {
+                    obj.setProperty("rimMap", FALLTHROUGH);
+                } else if (!material.rimMap.isEmpty()) {
+                    obj.setProperty("rimMap", material.rimMap);
+                }
+
+                // RIM_LIGHTING_MIX_VAL_BIT = graphics::MaterialKey::FlagBit::EXTRA_5_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EXTRA_5_BIT)) {
+                    obj.setProperty("rimLightingMix", FALLTHROUGH);
+                } else if (material.key._flags[graphics::MaterialKey::EXTRA_5_BIT]) {
+                    obj.setProperty("rimLightingMix", material.rimLightingMix);
+                }
+
+                // UV_ANIMATION_MASK_MAP_BIT = graphics::MaterialKey::FlagBit::LIGHT_MAP_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::LIGHT_MAP_BIT)) {
+                    obj.setProperty("uvAnimationMaskMap", FALLTHROUGH);
+                } else if (!material.uvAnimationMaskMap.isEmpty()) {
+                    obj.setProperty("uvAnimationMaskMap", material.uvAnimationMaskMap);
+                }
+
+                // UV_ANIMATION_SCROLL_VAL_BIT = graphics::MaterialKey::FlagBit::SCATTERING_VAL_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT)) {
+                    obj.setProperty("uvAnimationScrollXSpeed", FALLTHROUGH);
+                    obj.setProperty("uvAnimationScrollYSpeed", FALLTHROUGH);
+                    obj.setProperty("uvAnimationRotationSpeed", FALLTHROUGH);
+                } else if (material.key._flags[graphics::MaterialKey::SCATTERING_VAL_BIT]) {
+                    obj.setProperty("uvAnimationScrollXSpeed", material.uvAnimationScrollXSpeed);
+                    obj.setProperty("uvAnimationScrollYSpeed", material.uvAnimationScrollYSpeed);
+                    obj.setProperty("uvAnimationRotationSpeed", material.uvAnimationRotationSpeed);
+                }
+
+                // OUTLINE_WIDTH_MODE_VAL_BIT = graphics::Material::ExtraFlagBit::EXTRA_1_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::EXTRA_1_BIT)) {
+                    obj.setProperty("outlineWidthMode", FALLTHROUGH);
+                } else {
+                    obj.setProperty("outlineWidthMode", material.outlineWidthMode);
+                }
+
+                // OUTLINE_WIDTH_VAL_BIT = graphics::Material::ExtraFlagBit::EXTRA_2_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::EXTRA_2_BIT)) {
+                    obj.setProperty("outlineWidth", FALLTHROUGH);
+                } else {
+                    obj.setProperty("outlineWidth", material.outlineWidth);
+                }
+
+                // OUTLINE_VAL_BIT = graphics::Material::ExtraFlagBit::EXTRA_3_BIT
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::EXTRA_3_BIT)) {
+                    obj.setProperty("outline", FALLTHROUGH);
+                } else {
+                    obj.setProperty("outline", vec3ColorToScriptValue(engine, material.outline));
+                }
+            }
         } else if (material.model.toStdString() == graphics::Material::HIFI_SHADER_SIMPLE) {
             obj.setProperty("procedural", material.procedural);
         }
diff --git a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp
index 21454dfda0..3cf70915c3 100644
--- a/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp
+++ b/libraries/graphics-scripting/src/graphics-scripting/ScriptableModel.cpp
@@ -27,27 +27,50 @@ scriptable::ScriptableMaterial& scriptable::ScriptableMaterial::operator=(const
     opacity = material.opacity;
     albedo = material.albedo;
 
-    if (model.toStdString() == graphics::Material::HIFI_PBR) {
+    if (model.toStdString() == graphics::Material::HIFI_PBR || model.toStdString() == graphics::Material::VRM_MTOON) {
         opacityCutoff = material.opacityCutoff;
         opacityMapMode = material.opacityMapMode;
-        roughness = material.roughness;
-        metallic = material.metallic;
-        scattering = material.scattering;
-        unlit = material.unlit;
         emissive = material.emissive;
         emissiveMap = material.emissiveMap;
         albedoMap = material.albedoMap;
         opacityMap = material.opacityMap;
-        metallicMap = material.metallicMap;
-        specularMap = material.specularMap;
-        roughnessMap = material.roughnessMap;
-        glossMap = material.glossMap;
         normalMap = material.normalMap;
         bumpMap = material.bumpMap;
-        occlusionMap = material.occlusionMap;
-        lightMap = material.lightMap;
-        scatteringMap = material.scatteringMap;
         cullFaceMode = material.cullFaceMode;
+
+        if (model.toStdString() == graphics::Material::HIFI_PBR) {
+            roughness = material.roughness;
+            metallic = material.metallic;
+            scattering = material.scattering;
+            unlit = material.unlit;
+            metallicMap = material.metallicMap;
+            specularMap = material.specularMap;
+            roughnessMap = material.roughnessMap;
+            glossMap = material.glossMap;
+            occlusionMap = material.occlusionMap;
+            lightMap = material.lightMap;
+            scatteringMap = material.scatteringMap;
+        } else {
+            shade = material.shade;
+            shadeMap = material.shadeMap;
+            shadingShift = material.shadingShift;
+            shadingShiftMap = material.shadingShiftMap;
+            shadingToony = material.shadingToony;
+            matcap = material.matcap;
+            matcapMap = material.matcapMap;
+            parametricRim = material.parametricRim;
+            parametricRimFresnelPower = material.parametricRimFresnelPower;
+            parametricRimLift = material.parametricRimLift;
+            rimMap = material.rimMap;
+            rimLightingMix = material.rimLightingMix;
+            outlineWidthMode = material.outlineWidthMode;
+            outlineWidth = material.outlineWidth;
+            outline = material.outline;
+            uvAnimationMaskMap = material.uvAnimationMaskMap;
+            uvAnimationScrollXSpeed = material.uvAnimationScrollXSpeed;
+            uvAnimationScrollYSpeed = material.uvAnimationScrollYSpeed;
+            uvAnimationRotationSpeed = material.uvAnimationRotationSpeed;
+        }
     } else if (model.toStdString() == graphics::Material::HIFI_SHADER_SIMPLE) {
         procedural = material.procedural;
     }
@@ -67,13 +90,9 @@ scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPoint
         opacity = material->getOpacity();
         albedo = material->getAlbedo();
 
-        if (model.toStdString() == graphics::Material::HIFI_PBR) {
+        if (model.toStdString() == graphics::Material::HIFI_PBR || model.toStdString() == graphics::Material::VRM_MTOON) {
             opacityCutoff = material->getOpacityCutoff();
             opacityMapMode = QString(graphics::MaterialKey::getOpacityMapModeName(material->getOpacityMapMode()).c_str());
-            roughness = material->getRoughness();
-            metallic = material->getMetallic();
-            scattering = material->getScattering();
-            unlit = material->isUnlit();
             emissive = material->getEmissive();
 
             auto map = material->getTextureMap(graphics::Material::MapChannel::EMISSIVE_MAP);
@@ -89,24 +108,6 @@ scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPoint
                 }
             }
 
-            map = material->getTextureMap(graphics::Material::MapChannel::METALLIC_MAP);
-            if (map && map->getTextureSource()) {
-                if (map->getTextureSource()->getType() == image::TextureUsage::Type::METALLIC_TEXTURE) {
-                    metallicMap = map->getTextureSource()->getUrl().toString();
-                } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::SPECULAR_TEXTURE) {
-                    specularMap = map->getTextureSource()->getUrl().toString();
-                }
-            }
-
-            map = material->getTextureMap(graphics::Material::MapChannel::ROUGHNESS_MAP);
-            if (map && map->getTextureSource()) {
-                if (map->getTextureSource()->getType() == image::TextureUsage::Type::ROUGHNESS_TEXTURE) {
-                    roughnessMap = map->getTextureSource()->getUrl().toString();
-                } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::GLOSS_TEXTURE) {
-                    glossMap = map->getTextureSource()->getUrl().toString();
-                }
-            }
-
             map = material->getTextureMap(graphics::Material::MapChannel::NORMAL_MAP);
             if (map && map->getTextureSource()) {
                 if (map->getTextureSource()->getType() == image::TextureUsage::Type::NORMAL_TEXTURE) {
@@ -116,26 +117,92 @@ scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPoint
                 }
             }
 
-            map = material->getTextureMap(graphics::Material::MapChannel::OCCLUSION_MAP);
-            if (map && map->getTextureSource()) {
-                occlusionMap = map->getTextureSource()->getUrl().toString();
-            }
-
-            map = material->getTextureMap(graphics::Material::MapChannel::LIGHT_MAP);
-            if (map && map->getTextureSource()) {
-                lightMap = map->getTextureSource()->getUrl().toString();
-            }
-
-            map = material->getTextureMap(graphics::Material::MapChannel::SCATTERING_MAP);
-            if (map && map->getTextureSource()) {
-                scatteringMap = map->getTextureSource()->getUrl().toString();
-            }
-
             for (int i = 0; i < graphics::Material::NUM_TEXCOORD_TRANSFORMS; i++) {
                 texCoordTransforms[i] = material->getTexCoordTransform(i);
             }
 
             cullFaceMode = QString(graphics::MaterialKey::getCullFaceModeName(material->getCullFaceMode()).c_str());
+
+            if (model.toStdString() == graphics::Material::HIFI_PBR) {
+                roughness = material->getRoughness();
+                metallic = material->getMetallic();
+                scattering = material->getScattering();
+                unlit = material->isUnlit();
+
+                map = material->getTextureMap(graphics::Material::MapChannel::METALLIC_MAP);
+                if (map && map->getTextureSource()) {
+                    if (map->getTextureSource()->getType() == image::TextureUsage::Type::METALLIC_TEXTURE) {
+                        metallicMap = map->getTextureSource()->getUrl().toString();
+                    } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::SPECULAR_TEXTURE) {
+                        specularMap = map->getTextureSource()->getUrl().toString();
+                    }
+                }
+
+                map = material->getTextureMap(graphics::Material::MapChannel::ROUGHNESS_MAP);
+                if (map && map->getTextureSource()) {
+                    if (map->getTextureSource()->getType() == image::TextureUsage::Type::ROUGHNESS_TEXTURE) {
+                        roughnessMap = map->getTextureSource()->getUrl().toString();
+                    } else if (map->getTextureSource()->getType() == image::TextureUsage::Type::GLOSS_TEXTURE) {
+                        glossMap = map->getTextureSource()->getUrl().toString();
+                    }
+                }
+
+                map = material->getTextureMap(graphics::Material::MapChannel::OCCLUSION_MAP);
+                if (map && map->getTextureSource()) {
+                    occlusionMap = map->getTextureSource()->getUrl().toString();
+                }
+
+                map = material->getTextureMap(graphics::Material::MapChannel::LIGHT_MAP);
+                if (map && map->getTextureSource()) {
+                    lightMap = map->getTextureSource()->getUrl().toString();
+                }
+
+                map = material->getTextureMap(graphics::Material::MapChannel::SCATTERING_MAP);
+                if (map && map->getTextureSource()) {
+                    scatteringMap = map->getTextureSource()->getUrl().toString();
+                }
+            } else {
+                shade = material->getShade();
+                shadingShift = material->getShadingShift();
+                shadingToony = material->getShadingToony();
+                matcap = material->getMatcap();
+                parametricRim = material->getParametricRim();
+                parametricRimFresnelPower = material->getParametricRimFresnelPower();
+                parametricRimLift = material->getParametricRimLift();
+                rimLightingMix = material->getRimLightingMix();
+                outlineWidthMode = material->getOutlineWidthMode();
+                outlineWidth = material->getOutlineWidth();
+                outline = material->getOutline();
+                uvAnimationScrollXSpeed = material->getUVAnimationScrollXSpeed();
+                uvAnimationScrollYSpeed = material->getUVAnimationScrollYSpeed();
+                uvAnimationRotationSpeed = material->getUVAnimationRotationSpeed();
+
+                // See the mappings in ProceduralMatericalCache.h
+                map = material->getTextureMap(graphics::Material::MapChannel::ROUGHNESS_MAP);
+                if (map && map->getTextureSource()) {
+                    shadeMap = map->getTextureSource()->getUrl().toString();
+                }
+
+                map = material->getTextureMap(graphics::Material::MapChannel::METALLIC_MAP);
+                if (map && map->getTextureSource()) {
+                    shadingShiftMap = map->getTextureSource()->getUrl().toString();
+                }
+
+                map = material->getTextureMap(graphics::Material::MapChannel::OCCLUSION_MAP);
+                if (map && map->getTextureSource()) {
+                    matcapMap = map->getTextureSource()->getUrl().toString();
+                }
+
+                map = material->getTextureMap(graphics::Material::MapChannel::SCATTERING_MAP);
+                if (map && map->getTextureSource()) {
+                    rimMap = map->getTextureSource()->getUrl().toString();
+                }
+
+                map = material->getTextureMap(graphics::Material::MapChannel::LIGHT_MAP);
+                if (map && map->getTextureSource()) {
+                    uvAnimationMaskMap = map->getTextureSource()->getUrl().toString();
+                }
+            }
         } else if (model.toStdString() == graphics::Material::HIFI_SHADER_SIMPLE) {
             procedural = material->getProceduralString();
         }
diff --git a/libraries/graphics/src/graphics/Geometry.cpp b/libraries/graphics/src/graphics/Geometry.cpp
index 0fb2a0eb51..e46097207b 100644
--- a/libraries/graphics/src/graphics/Geometry.cpp
+++ b/libraries/graphics/src/graphics/Geometry.cpp
@@ -19,6 +19,8 @@ Mesh::Mesh() :
     _vertexBuffer(gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ)),
     _indexBuffer(gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::INDEX)),
     _partBuffer(gpu::Element(gpu::VEC4, gpu::UINT32, gpu::PART)) {
+    const uint32_t compactColor = 0xFFFFFFFF;
+    _colorBuffer->setData(sizeof(compactColor), (const gpu::Byte*) &compactColor);
 }
 
 Mesh::Mesh(const Mesh& mesh) :
@@ -26,7 +28,8 @@ Mesh::Mesh(const Mesh& mesh) :
     _vertexBuffer(mesh._vertexBuffer),
     _attributeBuffers(mesh._attributeBuffers),
     _indexBuffer(mesh._indexBuffer),
-    _partBuffer(mesh._partBuffer) {
+    _partBuffer(mesh._partBuffer),
+    _colorBuffer(mesh._colorBuffer) {
 }
 
 Mesh::~Mesh() {
@@ -39,6 +42,13 @@ void Mesh::setVertexFormatAndStream(const gpu::Stream::FormatPointer& vf, const
     auto attrib = _vertexFormat->getAttribute(gpu::Stream::POSITION);
     _vertexBuffer = BufferView(vbs->getBuffers()[attrib._channel], vbs->getOffsets()[attrib._channel], vbs->getBuffers()[attrib._channel]->getSize(),
         (gpu::uint16) vbs->getStrides()[attrib._channel], attrib._element);
+
+    // We require meshes to have a color attribute.  If they don't, we default to white.
+    if (!_vertexFormat->hasAttribute(gpu::Stream::COLOR)) {
+        gpu::Stream::Slot channelNum = (gpu::Stream::Slot)_vertexStream.getNumBuffers();
+        _vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), 0, gpu::Stream::PER_INSTANCE);
+        _vertexStream.addBuffer(_colorBuffer, 0, _vertexFormat->getChannels().at(channelNum)._stride);
+    }
 }
 
 void Mesh::setVertexBuffer(const BufferView& buffer) {
@@ -98,6 +108,12 @@ void Mesh::evalVertexStream() {
         _vertexStream.addBuffer(view._buffer, view._offset, stride);
         channelNum++;
     }
+
+    // We require meshes to have a color attribute.  If they don't, we default to white.
+    if (!_vertexFormat->hasAttribute(gpu::Stream::COLOR)) {
+        _vertexFormat->setAttribute(gpu::Stream::COLOR, channelNum, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), 0, gpu::Stream::PER_INSTANCE);
+        _vertexStream.addBuffer(_colorBuffer, 0, _vertexFormat->getChannels().at(channelNum)._stride);
+    }
 }
 
 void Mesh::setIndexBuffer(const BufferView& buffer) {
diff --git a/libraries/graphics/src/graphics/Geometry.h b/libraries/graphics/src/graphics/Geometry.h
index fe1981c0e9..dcaeaad271 100644
--- a/libraries/graphics/src/graphics/Geometry.h
+++ b/libraries/graphics/src/graphics/Geometry.h
@@ -15,6 +15,7 @@
 
 #include <AABox.h>
 
+#include <gpu/Forward.h>
 #include <gpu/Resource.h>
 #include <gpu/Stream.h>
 
@@ -28,7 +29,6 @@ typedef glm::vec3 Vec3;
 class Mesh;
 using MeshPointer = std::shared_ptr< Mesh >;
 
-
 class Mesh {
 public:
     const static Index PRIMITIVE_RESTART_INDEX = -1;
@@ -142,6 +142,8 @@ public:
     std::string modelName;
     std::string displayName;
 
+    gpu::BufferPointer getColorBuffer() const { return _colorBuffer; }
+
 protected:
 
     gpu::Stream::FormatPointer _vertexFormat;
@@ -154,6 +156,8 @@ protected:
 
     BufferView _partBuffer;
 
+    gpu::BufferPointer _colorBuffer { std::make_shared<gpu::Buffer>() };
+
     void evalVertexFormat();
     void evalVertexStream();
 
diff --git a/libraries/graphics/src/graphics/Material.cpp b/libraries/graphics/src/graphics/Material.cpp
index 836487de14..1061347e27 100644
--- a/libraries/graphics/src/graphics/Material.cpp
+++ b/libraries/graphics/src/graphics/Material.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 12/10/2014.
 //  Copyright 2014 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -60,7 +61,8 @@ bool MaterialKey::getCullFaceModeFromName(const std::string& modeName, CullFaceM
 }
 
 const std::string Material::HIFI_PBR { "hifi_pbr" };
-const std::string Material::HIFI_SHADER_SIMPLE { "hifi_shader_simple" };
+const std::string Material::HIFI_SHADER_SIMPLE{ "hifi_shader_simple" };
+const std::string Material::VRM_MTOON { "vrm_mtoon" };
 
 Material::Material() {
     for (int i = 0; i < NUM_TOTAL_FLAGS; i++) {
@@ -70,8 +72,8 @@ Material::Material() {
 
 Material::Material(const Material& material) :
     _name(material._name),
-    _model(material._model),
     _key(material._key),
+    _model(material._model),
     _emissive(material._emissive),
     _opacity(material._opacity),
     _albedo(material._albedo),
@@ -258,6 +260,17 @@ void Material::setTextureTransforms(const Transform& transform, MaterialMappingM
     _materialParams = glm::vec2(mode, repeat);
 }
 
+const glm::vec3 Material::DEFAULT_SHADE = glm::vec3(0.0f);
+const float Material::DEFAULT_SHADING_SHIFT = 0.0f;
+const float Material::DEFAULT_SHADING_TOONY = 0.9f;
+const glm::vec3 Material::DEFAULT_MATCAP = glm::vec3(1.0f);
+const glm::vec3 Material::DEFAULT_PARAMETRIC_RIM = glm::vec3(0.0f);
+const float Material::DEFAULT_PARAMETRIC_RIM_FRESNEL_POWER = 5.0f;
+const float Material::DEFAULT_PARAMETRIC_RIM_LIFT = 0.0f;
+const float Material::DEFAULT_RIM_LIGHTING_MIX = 1.0f;
+const float Material::DEFAULT_UV_ANIMATION_SCROLL_SPEED = 0.0f;
+const glm::vec3 Material::DEFAULT_OUTLINE = glm::vec3(0.0f);
+
 MultiMaterial::MultiMaterial() {
     Schema schema;
     _schemaBuffer = gpu::BufferView(std::make_shared<gpu::Buffer>(sizeof(Schema), (const gpu::Byte*) &schema, sizeof(Schema)));
@@ -311,3 +324,30 @@ bool MultiMaterial::anyReferenceMaterialsOrTexturesChanged() const {
 
     return false;
 }
+
+void MultiMaterial::setisMToon(bool isMToon) {
+    if (isMToon != _isMToon) {
+        if (isMToon) {
+            MToonSchema toonSchema;
+            _schemaBuffer = gpu::BufferView(std::make_shared<gpu::Buffer>(sizeof(MToonSchema), (const gpu::Byte*) &toonSchema, sizeof(MToonSchema)));
+        } else {
+            Schema schema;
+            _schemaBuffer = gpu::BufferView(std::make_shared<gpu::Buffer>(sizeof(Schema), (const gpu::Byte*) &schema, sizeof(Schema)));
+        }
+    }
+    _isMToon = isMToon;
+}
+
+void MultiMaterial::setMToonTime() {
+    assert(_isMToon);
+
+    // Some objects, like material entities, don't have persistent MultiMaterials to store this in, so we just store it once statically
+    static uint64_t mtoonStartTime;
+    static std::once_flag once;
+    std::call_once(once, [] {
+        mtoonStartTime = usecTimestampNow();
+    });
+
+    // Minimize floating point error by doing an integer division to milliseconds, before the floating point division to seconds
+    _schemaBuffer.edit<graphics::MultiMaterial::MToonSchema>()._time = (float)((usecTimestampNow() - mtoonStartTime) / USECS_PER_MSEC) / MSECS_PER_SECOND;
+}
diff --git a/libraries/graphics/src/graphics/Material.h b/libraries/graphics/src/graphics/Material.h
index 2eb4e0cbe1..fd9c76dd97 100644
--- a/libraries/graphics/src/graphics/Material.h
+++ b/libraries/graphics/src/graphics/Material.h
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 12/10/2014.
 //  Copyright 2014 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -34,6 +35,7 @@ typedef std::shared_ptr< TextureMap > TextureMapPointer;
 // Material Key is a coarse trait description of a material used to classify the materials
 class MaterialKey {
 public:
+    // Be careful changing these, they need to match up with the bits in graphics/Material.slh
    enum FlagBit {
         EMISSIVE_VAL_BIT = 0,
         UNLIT_VAL_BIT,
@@ -57,6 +59,12 @@ public:
         LIGHT_MAP_BIT,
         SCATTERING_MAP_BIT,
 
+        EXTRA_1_BIT,
+        EXTRA_2_BIT,
+        EXTRA_3_BIT,
+        EXTRA_4_BIT,
+        EXTRA_5_BIT,
+
         NUM_FLAGS,
     };
     typedef std::bitset<NUM_FLAGS> Flags;
@@ -419,6 +427,10 @@ public:
         MATERIAL_PARAMS,
         CULL_FACE_MODE,
 
+        EXTRA_1_BIT,
+        EXTRA_2_BIT,
+        EXTRA_3_BIT,
+
         NUM_TOTAL_FLAGS
     };
     std::unordered_map<uint, bool> getPropertyFallthroughs() { return _propertyFallthroughs; }
@@ -432,16 +444,43 @@ public:
 
     virtual bool isReference() const { return false; }
 
+    virtual bool isMToon() const { return false; }
+    static const glm::vec3 DEFAULT_SHADE;
+    virtual glm::vec3 getShade(bool SRGB = true) const { return glm::vec3(0.0f); }
+    static const float DEFAULT_SHADING_SHIFT;
+    virtual float getShadingShift() const { return 0.0f; }
+    static const float DEFAULT_SHADING_TOONY;
+    virtual float getShadingToony() const { return 0.0f; }
+    static const glm::vec3 DEFAULT_MATCAP;
+    virtual glm::vec3 getMatcap(bool SRGB = true) const { return glm::vec3(0.0f); }
+    static const glm::vec3 DEFAULT_PARAMETRIC_RIM;
+    virtual glm::vec3 getParametricRim(bool SRGB = true) const { return glm::vec3(0.0f); }
+    static const float DEFAULT_PARAMETRIC_RIM_FRESNEL_POWER;
+    virtual float getParametricRimFresnelPower() const { return 0.0f; }
+    static const float DEFAULT_PARAMETRIC_RIM_LIFT;
+    virtual float getParametricRimLift() const { return 0.0f; }
+    static const float DEFAULT_RIM_LIGHTING_MIX;
+    virtual float getRimLightingMix() const { return 0.0f; }
+    static const float DEFAULT_UV_ANIMATION_SCROLL_SPEED;
+    virtual float getUVAnimationScrollXSpeed() const { return 0.0f; }
+    virtual float getUVAnimationScrollYSpeed() const { return 0.0f; }
+    virtual float getUVAnimationRotationSpeed() const { return 0.0f; }
+
+    static const glm::vec3 DEFAULT_OUTLINE;
+    virtual uint8_t getOutlineWidthMode() { return 0; }
+    virtual float getOutlineWidth() { return 0.0f; }
+    virtual glm::vec3 getOutline(bool SRGB = true) const { return glm::vec3(0.0f); }
+
     static const std::string HIFI_PBR;
     static const std::string HIFI_SHADER_SIMPLE;
+    static const std::string VRM_MTOON;
 
 protected:
     std::string _name { "" };
+    mutable MaterialKey _key { 0 };
+    std::string _model { HIFI_PBR };
 
 private:
-    std::string _model { HIFI_PBR };
-    mutable MaterialKey _key { 0 };
-
     // Material properties
     glm::vec3 _emissive { DEFAULT_EMISSIVE };
     float _opacity { DEFAULT_OPACITY };
@@ -525,12 +564,12 @@ public:
         // Texture Coord Transform Array
         glm::mat4 _texcoordTransforms[Material::NUM_TEXCOORD_TRANSFORMS];
 
-        glm::vec2 _lightmapParams { 0.0, 1.0 };
-
         // x: material mode (0 for UV, 1 for PROJECTED)
         // y: 1 for texture repeat, 0 for discard outside of 0 - 1
         glm::vec2 _materialParams { 0.0, 1.0 };
 
+        glm::vec2 _lightmapParams { 0.0, 1.0 };
+
         Schema() {
             for (auto& transform : _texcoordTransforms) {
                 transform = glm::mat4();
@@ -538,8 +577,68 @@ public:
         }
     };
 
+    class MToonSchema {
+    public:
+        glm::vec3 _emissive { Material::DEFAULT_EMISSIVE }; // No Emissive
+        float _opacity { Material::DEFAULT_OPACITY }; // Opacity = 1 => Not Transparent
+
+        glm::vec3 _albedo { Material::DEFAULT_ALBEDO }; // Grey albedo => isAlbedo
+        float _opacityCutoff { Material::DEFAULT_OPACITY_CUTOFF }; // Opacity cutoff applyed when using opacityMap as Mask
+
+        glm::vec3 _shade { Material::DEFAULT_SHADE };
+        float _shadingShift { Material::DEFAULT_SHADING_SHIFT };
+
+        glm::vec3 _matcap { Material::DEFAULT_MATCAP };
+        float _shadingToony { Material::DEFAULT_SHADING_TOONY };
+
+        glm::vec3 _parametricRim { Material::DEFAULT_PARAMETRIC_RIM };
+        float _parametricRimFresnelPower { Material::DEFAULT_PARAMETRIC_RIM_FRESNEL_POWER };
+
+        float _parametricRimLift { Material::DEFAULT_PARAMETRIC_RIM_LIFT };
+        float _rimLightingMix { Material::DEFAULT_RIM_LIGHTING_MIX };
+        glm::vec2 _uvAnimationScrollSpeed { Material::DEFAULT_UV_ANIMATION_SCROLL_SPEED };
+
+        float _uvAnimationScrollRotationSpeed { Material::DEFAULT_UV_ANIMATION_SCROLL_SPEED };
+        float _time { 0.0f };
+        uint32_t _key { 0 }; // a copy of the materialKey
+        float _spare { 0.0f };
+
+        // Texture Coord Transform Array
+        glm::mat4 _texcoordTransforms[Material::NUM_TEXCOORD_TRANSFORMS];
+
+        // x: material mode (0 for UV, 1 for PROJECTED)
+        // y: 1 for texture repeat, 0 for discard outside of 0 - 1
+        glm::vec2 _materialParams { 0.0, 1.0 };
+
+        MToonSchema() {
+            for (auto& transform : _texcoordTransforms) {
+                transform = glm::mat4();
+            }
+        }
+    };
+
     gpu::BufferView& getSchemaBuffer() { return _schemaBuffer; }
-    graphics::MaterialKey getMaterialKey() const { return graphics::MaterialKey(_schemaBuffer.get<graphics::MultiMaterial::Schema>()._key); }
+    graphics::MaterialKey getMaterialKey() const {
+        if (_isMToon) {
+            return graphics::MaterialKey(_schemaBuffer.get<graphics::MultiMaterial::MToonSchema>()._key);
+        } else {
+            return graphics::MaterialKey(_schemaBuffer.get<graphics::MultiMaterial::Schema>()._key);
+        }
+    }
+    glm::vec4 getColor() const {
+        glm::vec3 albedo;
+        float opacity;
+        if (_isMToon) {
+            const auto& schema = _schemaBuffer.get<graphics::MultiMaterial::MToonSchema>();
+            albedo = schema._albedo;
+            opacity = schema._opacity;
+        } else {
+            const auto& schema = _schemaBuffer.get<graphics::MultiMaterial::Schema>();
+            albedo = schema._albedo;
+            opacity = schema._opacity;
+        }
+        return glm::vec4(ColorUtils::tosRGBVec3(albedo), opacity);
+    }
     const gpu::TextureTablePointer& getTextureTable() const { return _textureTable; }
 
     void setCullFaceMode(graphics::MaterialKey::CullFaceMode cullFaceMode) { _cullFaceMode = cullFaceMode; }
@@ -559,6 +658,18 @@ public:
     void addReferenceTexture(const std::function<gpu::TexturePointer()>& textureOperator);
     void addReferenceMaterial(const std::function<graphics::MaterialPointer()>& materialOperator);
 
+    void setisMToon(bool isMToon);
+    bool isMToon() const { return _isMToon; }
+    void setMToonTime();
+    bool hasOutline() const { return _outlineWidthMode != 0 && _outlineWidth > 0.0f; }
+    uint8_t getOutlineWidthMode() const { return _outlineWidthMode; }
+    float getOutlineWidth() const { return _outlineWidth; }
+    glm::vec3 getOutline() const { return _outline; }
+    void resetOutline() { _outlineWidthMode = 0; _outlineWidth = 0.0f; _outline = glm::vec3(0.0f); }
+    void setOutlineWidthMode(uint8_t mode) { _outlineWidthMode = mode; }
+    void setOutlineWidth(float width) { _outlineWidth = width; }
+    void setOutline(const glm::vec3& outline) { _outline = outline; }
+
 private:
     gpu::BufferView _schemaBuffer;
     graphics::MaterialKey::CullFaceMode _cullFaceMode { graphics::Material::DEFAULT_CULL_FACE_MODE };
@@ -576,6 +687,11 @@ private:
 
     std::vector<std::pair<std::function<gpu::TexturePointer()>, gpu::TexturePointer>> _referenceTextures;
     std::vector<std::pair<std::function<graphics::MaterialPointer()>, graphics::MaterialPointer>> _referenceMaterials;
+
+    bool _isMToon { false };
+    uint8_t _outlineWidthMode { 0 };
+    float _outlineWidth { 0.0f };
+    glm::vec3 _outline { graphics::Material::DEFAULT_OUTLINE };
 };
 
 };
diff --git a/libraries/graphics/src/graphics/Material.slh b/libraries/graphics/src/graphics/Material.slh
index 274dbc1cdd..4d4dcde34c 100644
--- a/libraries/graphics/src/graphics/Material.slh
+++ b/libraries/graphics/src/graphics/Material.slh
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 12/16/14.
 //  Copyright 2013 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -18,8 +19,10 @@ const int MAX_TEXCOORDS = 2;
 struct TexMapArray { 
     mat4 _texcoordTransforms0;
     mat4 _texcoordTransforms1;
-    vec2 _lightmapParams;
     vec2 _materialParams;
+<@if not HIFI_USE_MTOON@>
+    vec2 _lightmapParams;
+<@endif@>
 };
 
 <@func declareMaterialTexMapArrayBuffer()@>
@@ -45,11 +48,24 @@ struct TexMapArray {
 // The material values (at least the material key) must be precisely bitwise accurate
 // to what is provided by the uniform buffer, or the material key has the wrong bits
 
+<@if not HIFI_USE_MTOON@>
 struct Material {
     vec4 _emissiveOpacity;
     vec4 _albedoRoughness;
     vec4 _metallicScatteringOpacityCutoffKey;
 };
+<@else@>
+struct Material {
+    vec4 _emissiveOpacity;
+    vec4 _albedoOpacityCutoff;
+
+    vec4 _shadeShadingShift;
+    vec4 _matcapShadingToony;
+    vec4 _parametricRimAndFresnelPower;
+    vec4 _parametricRimLiftMixUVAnimationScrollSpeedXY;
+    vec4 _uvAnimationScrollRotationSpeedTimeKeySpare;
+};
+<@endif@>
 
 LAYOUT_STD140(binding=GRAPHICS_BUFFER_MATERIAL) uniform materialBuffer {
     Material _mat;
@@ -63,39 +79,91 @@ TexMapArray getTexMapArray() {
     return _texMapArray;
 }
 
-vec3 getMaterialEmissive(Material m) { return m._emissiveOpacity.rgb; }
-float getMaterialOpacity(Material m) { return m._emissiveOpacity.a; }
+<@if not HIFI_USE_MTOON@>
+    vec3 getMaterialEmissive(Material m) { return m._emissiveOpacity.rgb; }
+    float getMaterialOpacity(Material m) { return m._emissiveOpacity.a; }
 
-vec3 getMaterialAlbedo(Material m) { return m._albedoRoughness.rgb; }
-float getMaterialRoughness(Material m) { return m._albedoRoughness.a; }
-float getMaterialShininess(Material m) { return 1.0 - getMaterialRoughness(m); }
+    vec3 getMaterialAlbedo(Material m) { return m._albedoRoughness.rgb; }
+    float getMaterialRoughness(Material m) { return m._albedoRoughness.a; }
+    float getMaterialShininess(Material m) { return 1.0 - getMaterialRoughness(m); }
 
-float getMaterialMetallic(Material m) { return m._metallicScatteringOpacityCutoffKey.x; }
-float getMaterialScattering(Material m) { return m._metallicScatteringOpacityCutoffKey.y; }
-float getMaterialOpacityCutoff(Material m) { return m._metallicScatteringOpacityCutoffKey.z; }
+    float getMaterialMetallic(Material m) { return m._metallicScatteringOpacityCutoffKey.x; }
+    float getMaterialScattering(Material m) { return m._metallicScatteringOpacityCutoffKey.y; }
+    float getMaterialOpacityCutoff(Material m) { return m._metallicScatteringOpacityCutoffKey.z; }
 
-BITFIELD getMaterialKey(Material m) { return floatBitsToInt(m._metallicScatteringOpacityCutoffKey.w); }
+    BITFIELD getMaterialKey(Material m) { return floatBitsToInt(m._metallicScatteringOpacityCutoffKey.w); }
 
-const BITFIELD EMISSIVE_VAL_BIT              = 0x00000001;
-const BITFIELD UNLIT_VAL_BIT                 = 0x00000002;
-const BITFIELD ALBEDO_VAL_BIT                = 0x00000004;
-const BITFIELD METALLIC_VAL_BIT              = 0x00000008;
-const BITFIELD GLOSSY_VAL_BIT                = 0x00000010;
-const BITFIELD OPACITY_VAL_BIT               = 0x00000020;
-const BITFIELD OPACITY_MASK_MAP_BIT          = 0x00000040;
-const BITFIELD OPACITY_TRANSLUCENT_MAP_BIT   = 0x00000080;
-const BITFIELD OPACITY_MAP_MODE_BIT          = 0x00000100;
-const BITFIELD OPACITY_CUTOFF_VAL_BIT        = 0x00000200;
-const BITFIELD SCATTERING_VAL_BIT            = 0x00000400;
+    const BITFIELD EMISSIVE_VAL_BIT              = 0x00000001;
+    const BITFIELD UNLIT_VAL_BIT                 = 0x00000002;
+    const BITFIELD ALBEDO_VAL_BIT                = 0x00000004;
+    const BITFIELD METALLIC_VAL_BIT              = 0x00000008;
+    const BITFIELD GLOSSY_VAL_BIT                = 0x00000010;
+    const BITFIELD OPACITY_VAL_BIT               = 0x00000020;
+    const BITFIELD OPACITY_MASK_MAP_BIT          = 0x00000040;
+    const BITFIELD OPACITY_TRANSLUCENT_MAP_BIT   = 0x00000080;
+    const BITFIELD OPACITY_MAP_MODE_BIT          = 0x00000100;
+    const BITFIELD OPACITY_CUTOFF_VAL_BIT        = 0x00000200;
+    const BITFIELD SCATTERING_VAL_BIT            = 0x00000400;
 
 
-const BITFIELD EMISSIVE_MAP_BIT              = 0x00000800;
-const BITFIELD ALBEDO_MAP_BIT                = 0x00001000;
-const BITFIELD METALLIC_MAP_BIT              = 0x00002000;
-const BITFIELD ROUGHNESS_MAP_BIT             = 0x00004000;
-const BITFIELD NORMAL_MAP_BIT                = 0x00008000;
-const BITFIELD OCCLUSION_MAP_BIT             = 0x00010000;
-const BITFIELD LIGHTMAP_MAP_BIT              = 0x00020000;
-const BITFIELD SCATTERING_MAP_BIT            = 0x00040000;
+    const BITFIELD EMISSIVE_MAP_BIT              = 0x00000800;
+    const BITFIELD ALBEDO_MAP_BIT                = 0x00001000;
+    const BITFIELD METALLIC_MAP_BIT              = 0x00002000;
+    const BITFIELD ROUGHNESS_MAP_BIT             = 0x00004000;
+    const BITFIELD NORMAL_MAP_BIT                = 0x00008000;
+    const BITFIELD OCCLUSION_MAP_BIT             = 0x00010000;
+    const BITFIELD LIGHTMAP_MAP_BIT              = 0x00020000;
+    const BITFIELD SCATTERING_MAP_BIT            = 0x00040000;
+<@else@>
+    vec3 getMaterialEmissive(Material m) { return m._emissiveOpacity.rgb; }
+    float getMaterialOpacity(Material m) { return m._emissiveOpacity.a; }
+
+    vec3 getMaterialAlbedo(Material m) { return m._albedoOpacityCutoff.rgb; }
+    float getMaterialOpacityCutoff(Material m) { return m._albedoOpacityCutoff.z; }
+
+    vec3 getMaterialShade(Material m) { return m._shadeShadingShift.rgb; }
+    float getMaterialShadingShift(Material m) { return m._shadeShadingShift.a; }
+
+    vec3 getMaterialMatcap(Material m) { return m._matcapShadingToony.rgb; }
+    float getMaterialShadingToony(Material m) { return m._matcapShadingToony.a; }
+
+    vec3 getMaterialParametricRim(Material m) { return m._parametricRimAndFresnelPower.rgb; }
+    float getMaterialParametricRimFresnelPower(Material m) { return m._parametricRimAndFresnelPower.a; }
+
+    float getMaterialParametricRimLift(Material m) { return m._parametricRimLiftMixUVAnimationScrollSpeedXY.r; }
+    float getMaterialRimLightingMix(Material m) { return m._parametricRimLiftMixUVAnimationScrollSpeedXY.g; }
+
+    vec3 getMaterialUVScrollSpeed(Material m) { return vec3(m._parametricRimLiftMixUVAnimationScrollSpeedXY.ba, m._uvAnimationScrollRotationSpeedTimeKeySpare.r); }
+    float getMaterialTime(Material m) { return m._uvAnimationScrollRotationSpeedTimeKeySpare.g; }
+
+    BITFIELD getMaterialKey(Material m) { return floatBitsToInt(m._uvAnimationScrollRotationSpeedTimeKeySpare.b); }
+
+    const BITFIELD EMISSIVE_VAL_BIT              = 0x00000001;
+    const BITFIELD SHADE_VAL_BIT                 = 0x00000002;
+    const BITFIELD ALBEDO_VAL_BIT                = 0x00000004;
+    const BITFIELD SHADING_SHIFT_VAL_BIT         = 0x00000008;
+    const BITFIELD SHADING_TOONY_VAL_BIT         = 0x00000010;
+    const BITFIELD OPACITY_VAL_BIT               = 0x00000020;
+    const BITFIELD OPACITY_MASK_MAP_BIT          = 0x00000040;
+    const BITFIELD OPACITY_TRANSLUCENT_MAP_BIT   = 0x00000080;
+    const BITFIELD OPACITY_MAP_MODE_BIT          = 0x00000100;
+    const BITFIELD OPACITY_CUTOFF_VAL_BIT        = 0x00000200;
+    const BITFIELD UV_ANIMATION_SCROLL_VAL_BIT   = 0x00000400;
+
+    const BITFIELD EMISSIVE_MAP_BIT              = 0x00000800;
+    const BITFIELD ALBEDO_MAP_BIT                = 0x00001000;
+    const BITFIELD SHADING_SHIFT_MAP_BIT         = 0x00002000;
+    const BITFIELD SHADE_MAP_BIT                 = 0x00004000;
+    const BITFIELD NORMAL_MAP_BIT                = 0x00008000;
+    const BITFIELD MATCAP_MAP_BIT                = 0x00010000;
+    const BITFIELD UV_ANIMATION_MASK_MAP_BIT     = 0x00020000;
+    const BITFIELD RIM_MAP_BIT                   = 0x00040000;
+
+    const BITFIELD MATCAP_VAL_BIT                = 0x00080000;
+    const BITFIELD PARAMETRIC_RIM_VAL_BIT        = 0x00100000;
+    const BITFIELD PARAMETRIC_RIM_POWER_VAL_BIT  = 0x00200000;
+    const BITFIELD PARAMETRIC_RIM_LIFT_VAL_BIT   = 0x00400000;
+    const BITFIELD RIM_LIGHTING_MIX_VAL_BIT      = 0x00800000;
+<@endif@>
 
 <@endif@>
diff --git a/libraries/graphics/src/graphics/MaterialTextures.slh b/libraries/graphics/src/graphics/MaterialTextures.slh
index cb83f7d9cf..083a1146be 100644
--- a/libraries/graphics/src/graphics/MaterialTextures.slh
+++ b/libraries/graphics/src/graphics/MaterialTextures.slh
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 2/22/16
 //  Copyright 2016 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -14,10 +15,70 @@
 <@include graphics/ShaderConstants.h@>
 <@include graphics/Material.slh@>
 
-<@func declareMaterialTextures(withAlbedo, withRoughness, withNormal, withMetallic, withEmissive, withOcclusion, withScattering)@>
-
 #define TAA_TEXTURE_LOD_BIAS    -1.0
 
+<@func evalMaterialNormalLOD(fragPosES, fetchedNormal, interpolatedNormal, interpolatedTangent, normal)@>
+{
+    vec3 normalizedNormal = normalize(<$interpolatedNormal$>.xyz);
+    vec3 normalizedTangent = normalize(<$interpolatedTangent$>.xyz);
+    vec3 normalizedBitangent = cross(normalizedNormal, normalizedTangent);
+    // attenuate the normal map divergence from the mesh normal based on distance
+    // The attenuation range [30,100] meters from the eye is arbitrary for now
+    vec3 localNormal = mix(<$fetchedNormal$>, vec3(0.0, 1.0, 0.0), smoothstep(30.0, 100.0, (-<$fragPosES$>).z));
+    <$normal$> = vec3(normalizedBitangent * localNormal.x + normalizedNormal * localNormal.y + normalizedTangent * localNormal.z);
+}
+<@endfunc@>
+
+<@func evalMaterialAlbedo(fetchedAlbedo, materialAlbedo, matKey, albedo)@>
+{
+    <$albedo$>.xyz = mix(vec3(1.0), <$materialAlbedo$>, float((<$matKey$> & ALBEDO_VAL_BIT) != 0));
+    <$albedo$>.xyz *= mix(vec3(1.0), <$fetchedAlbedo$>.xyz, float((<$matKey$> & ALBEDO_MAP_BIT) != 0));
+}
+<@endfunc@>
+
+<@func evalMaterialOpacityMask(fetchedOpacity, materialOpacityCutoff, materialOpacity, matKey, opacity)@>
+{
+    // This path only valid for opaque or texel opaque material
+    <$opacity$> = mix(<$materialOpacity$>,
+                      step(<$materialOpacityCutoff$>, <$fetchedOpacity$>),
+                      float((<$matKey$> & OPACITY_MASK_MAP_BIT) != 0));
+}
+<@endfunc@>
+
+<@func evalMaterialOpacity(fetchedOpacity, materialOpacityCutoff, materialOpacity, matKey, opacity)@>
+{
+    // This path only valid for transparent material
+    <$opacity$> = mix(<$fetchedOpacity$>,
+                          step(<$materialOpacityCutoff$>, <$fetchedOpacity$>),
+                          float((<$matKey$> & OPACITY_MASK_MAP_BIT) != 0))
+                       * <$materialOpacity$>;
+}
+<@endfunc@>
+
+<@func evalMaterialEmissive(fetchedEmissive, materialEmissive, matKey, emissive)@>
+{
+    <$emissive$> = mix(<$materialEmissive$>, <$fetchedEmissive$>, float((<$matKey$> & EMISSIVE_MAP_BIT) != 0));
+}
+<@endfunc@>
+
+<@func discardTransparent(opacity)@>
+{
+    if (<$opacity$> < 1.0) {
+        discard;
+    }
+}
+<@endfunc@>
+<@func discardInvisible(opacity)@>
+{
+    if (<$opacity$> <= 0.0) {
+        discard;
+    }
+}
+<@endfunc@>
+
+<@if not HIFI_USE_MTOON@>
+<@func declareMaterialTextures(withAlbedo, withRoughness, withNormal, withMetallic, withEmissive, withOcclusion, withScattering)@>
+
 <@include gpu/TextureTable.slh@>
 
 #ifdef GPU_TEXTURE_TABLE_BINDLESS
@@ -41,14 +102,6 @@ vec4 fetchAlbedoMap(vec2 uv) {
 }
 <@endif@>
 
-<@if withRoughness@>
-#define roughnessMap 4
-float fetchRoughnessMap(vec2 uv) {
-    // Should take into account TAA_TEXTURE_LOD_BIAS?
-    return tableTexValue(matTex, roughnessMap, uv).r;
-}
-<@endif@>
-
 <@if withNormal@>
 #define normalMap 1
 vec3 fetchNormalMap(vec2 uv) {
@@ -73,6 +126,14 @@ vec3 fetchEmissiveMap(vec2 uv) {
 }
 <@endif@>
 
+<@if withRoughness@>
+#define roughnessMap 4
+float fetchRoughnessMap(vec2 uv) {
+    // Should take into account TAA_TEXTURE_LOD_BIAS?
+    return tableTexValue(matTex, roughnessMap, uv).r;
+}
+<@endif@>
+
 <@if withOcclusion@>
 #define occlusionMap 5
 float fetchOcclusionMap(vec2 uv) {
@@ -98,13 +159,6 @@ vec4 fetchAlbedoMap(vec2 uv) {
 }
 <@endif@>
 
-<@if withRoughness@>
-LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_ROUGHNESS) uniform sampler2D roughnessMap;
-float fetchRoughnessMap(vec2 uv) {
-    return (texture(roughnessMap, uv, TAA_TEXTURE_LOD_BIAS).r);
-}
-<@endif@>
-
 <@if withNormal@>
 LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_NORMAL) uniform sampler2D normalMap;
 vec3 fetchNormalMap(vec2 uv) {
@@ -129,6 +183,13 @@ vec3 fetchEmissiveMap(vec2 uv) {
 }
 <@endif@>
 
+<@if withRoughness@>
+LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_ROUGHNESS) uniform sampler2D roughnessMap;
+float fetchRoughnessMap(vec2 uv) {
+    return (texture(roughnessMap, uv, TAA_TEXTURE_LOD_BIAS).r);
+}
+<@endif@>
+
 <@if withOcclusion@>
 LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_OCCLUSION) uniform sampler2D occlusionMap;
 float fetchOcclusionMap(vec2 uv) {
@@ -183,7 +244,6 @@ float fetchScatteringMap(vec2 uv) {
 <@endfunc@>
 
 
-
 <@func declareMaterialLightmap()@>
 
 <$declareMaterialTexMapArrayBuffer()$>
@@ -195,59 +255,6 @@ vec3 fetchLightMap(vec2 uv) {
 }
 <@endfunc@>
 
-<@func evalMaterialNormalLOD(fragPosES, fetchedNormal, interpolatedNormal, interpolatedTangent, normal)@>
-{
-    vec3 normalizedNormal = normalize(<$interpolatedNormal$>.xyz);
-    vec3 normalizedTangent = normalize(<$interpolatedTangent$>.xyz);
-    vec3 normalizedBitangent = cross(normalizedNormal, normalizedTangent);
-    // attenuate the normal map divergence from the mesh normal based on distance
-    // The attenuation range [30,100] meters from the eye is arbitrary for now
-    vec3 localNormal = mix(<$fetchedNormal$>, vec3(0.0, 1.0, 0.0), smoothstep(30.0, 100.0, (-<$fragPosES$>).z));
-    <$normal$> = vec3(normalizedBitangent * localNormal.x + normalizedNormal * localNormal.y + normalizedTangent * localNormal.z);
-}
-<@endfunc@>
-
-<@func evalMaterialAlbedo(fetchedAlbedo, materialAlbedo, matKey, albedo)@>
-{
-    <$albedo$>.xyz = mix(vec3(1.0), <$materialAlbedo$>, float((<$matKey$> & ALBEDO_VAL_BIT) != 0));
-    <$albedo$>.xyz *= mix(vec3(1.0), <$fetchedAlbedo$>.xyz, float((<$matKey$> & ALBEDO_MAP_BIT) != 0));
-}
-<@endfunc@>
-
-<@func evalMaterialOpacityMask(fetchedOpacity, materialOpacityCutoff, materialOpacity, matKey, opacity)@>
-{
-    // This path only valid for opaque or texel opaque material
-    <$opacity$> = mix(<$materialOpacity$>,
-                      step(<$materialOpacityCutoff$>, <$fetchedOpacity$>),
-                      float((<$matKey$> & OPACITY_MASK_MAP_BIT) != 0));
-}
-<@endfunc@>
-
-<@func evalMaterialOpacity(fetchedOpacity, materialOpacityCutoff, materialOpacity, matKey, opacity)@>
-{
-    // This path only valid for transparent material
-    <$opacity$> = mix(<$fetchedOpacity$>,
-                          step(<$materialOpacityCutoff$>, <$fetchedOpacity$>),
-                          float((<$matKey$> & OPACITY_MASK_MAP_BIT) != 0))
-                       * <$materialOpacity$>;
-}
-<@endfunc@>
-
-<@func discardTransparent(opacity)@>
-{
-    if (<$opacity$> < 1.0) {
-        discard;
-    }
-}
-<@endfunc@>
-<@func discardInvisible(opacity)@>
-{
-    if (<$opacity$> <= 0.0) {
-        discard;
-    }
-}
-<@endfunc@>
-
 <@func evalMaterialRoughness(fetchedRoughness, materialRoughness, matKey, roughness)@>
 {
     <$roughness$> = mix(<$materialRoughness$>, <$fetchedRoughness$>, float((<$matKey$> & ROUGHNESS_MAP_BIT) != 0));
@@ -260,12 +267,6 @@ vec3 fetchLightMap(vec2 uv) {
 }
 <@endfunc@>
 
-<@func evalMaterialEmissive(fetchedEmissive, materialEmissive, matKey, emissive)@>
-{
-    <$emissive$> = mix(<$materialEmissive$>, <$fetchedEmissive$>, float((<$matKey$> & EMISSIVE_MAP_BIT) != 0));
-}
-<@endfunc@>
-
 <@func evalMaterialOcclusion(fetchedOcclusion, matKey, occlusion)@>
 {
     <$occlusion$> = <$fetchedOcclusion$>;
@@ -277,5 +278,214 @@ vec3 fetchLightMap(vec2 uv) {
     <$scattering$> = mix(<$materialScattering$>, <$fetchedScattering$>, float((<$matKey$> & SCATTERING_MAP_BIT) != 0));
 }
 <@endfunc@>
+<@else@>
+<@func declareMToonMaterialTextures(withAlbedo, withNormal, withShade, withEmissive, withShadingShift, withMatcap, withRim, withUVAnimationMask)@>
 
-<@endif@>
\ No newline at end of file
+<@include gpu/TextureTable.slh@>
+
+#ifdef GPU_TEXTURE_TABLE_BINDLESS
+
+TextureTable(0, matTex);
+<!
+    ALBEDO = 0,
+    NORMAL, 1
+    SHADE, 2
+    EMISSIVE, 3
+    SHADING_SHIFT, 4
+    MATCAP, 5
+    RIM, 6
+    UV_ANIMATION_MASK, 7
+!>
+
+<@if withAlbedo@>
+#define albedoMap 0
+vec4 fetchAlbedoMap(vec2 uv) {
+    return tableTexValue(matTex, albedoMap, uv);
+}
+<@endif@>
+
+<@if withNormal@>
+#define normalMap 1
+vec3 fetchNormalMap(vec2 uv) {
+    return tableTexValue(matTex, normalMap, uv).xyz;
+}
+<@endif@>
+
+<@if withShade@>
+#define shadeMap 2
+vec3 fetchShadeMap(vec2 uv) {
+    return tableTexValue(matTex, shadeMap, uv).rgb;
+}
+<@endif@>
+
+<@if withEmissive@>
+#define emissiveMap 3
+vec3 fetchEmissiveMap(vec2 uv) {
+    return tableTexValue(matTex, emissiveMap, uv).rgb;
+}
+<@endif@>
+
+<@if withShadingShift@>
+#define shadingShiftMap 4
+float fetchShadingShiftMap(vec2 uv) {
+    return tableTexValue(matTex, shadingShiftMap, uv).r;
+}
+<@endif@>
+
+<@if withMatcap@>
+#define matcapMap 5
+vec3 fetchMatcapMap(vec2 uv) {
+    return tableTexValue(matTex, matcapMap, uv).rgb;
+}
+<@endif@>
+
+<@if withRim@>
+#define rimMap 6
+vec3 fetchRimMap(vec2 uv) {
+    return tableTexValue(matTex, rimMap, uv).rgb;
+}
+<@endif@>
+
+<@if withUVAnimationMask@>
+#define uvAnimationMaskMap 7
+float fetchUVAnimationMaskMap(vec2 uv) {
+    return tableTexValue(matTex, uvAnimationMaskMap, uv).r;
+}
+<@endif@>
+
+#else
+
+<@if withAlbedo@>
+LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_ALBEDO) uniform sampler2D albedoMap;
+vec4 fetchAlbedoMap(vec2 uv) {
+    return texture(albedoMap, uv, TAA_TEXTURE_LOD_BIAS);
+}
+<@endif@>
+
+<@if withNormal@>
+LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_NORMAL) uniform sampler2D normalMap;
+vec3 fetchNormalMap(vec2 uv) {
+    // unpack normal, swizzle to get into hifi tangent space with Y axis pointing out
+    vec2 t = 2.0 * (texture(normalMap, uv, TAA_TEXTURE_LOD_BIAS).rg - vec2(0.5, 0.5));
+    vec2 t2 = t*t;
+    return vec3(t.x, sqrt(max(0.0, 1.0 - t2.x - t2.y)), t.y);
+}
+<@endif@>
+
+<@if withShade@>
+LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_SHADE) uniform sampler2D shadeMap;
+vec3 fetchShadeMap(vec2 uv) {
+    return texture(shadeMap, uv, TAA_TEXTURE_LOD_BIAS).rgb;
+}
+<@endif@>
+
+<@if withEmissive@>
+LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_EMISSIVE_LIGHTMAP) uniform sampler2D emissiveMap;
+vec3 fetchEmissiveMap(vec2 uv) {
+    return texture(emissiveMap, uv, TAA_TEXTURE_LOD_BIAS).rgb;
+}
+<@endif@>
+
+<@if withShadingShift@>
+LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_SHADING_SHIFT) uniform sampler2D shadingShiftMap;
+float fetchShadingShiftMap(vec2 uv) {
+    return texture(shadingShiftMap, uv, TAA_TEXTURE_LOD_BIAS).r;
+}
+<@endif@>
+
+<@if withMatcap@>
+LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_MATCAP) uniform sampler2D matcapMap;
+vec3 fetchMatcapMap(vec2 uv) {
+    return texture(matcapMap, uv, TAA_TEXTURE_LOD_BIAS).rgb;
+}
+<@endif@>
+
+<@if withRim@>
+LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_RIM) uniform sampler2D rimMap;
+vec3 fetchRimMap(vec2 uv) {
+    return texture(rimMap, uv, TAA_TEXTURE_LOD_BIAS).rgb;
+}
+<@endif@>
+
+<@if withUVAnimationMask@>
+LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_UV_ANIMATION_MASK) uniform sampler2D uvAnimationMaskMap;
+float fetchUVAnimationMaskMap(vec2 uv) {
+    return texture(uvAnimationMaskMap, uv, TAA_TEXTURE_LOD_BIAS).r;
+}
+<@endif@>
+
+#endif
+
+<@endfunc@>
+
+<@func fetchMToonMaterialTexturesCoord0(matKey, texcoord0, albedo, normal, shade, emissive, shadingShift, rim, uvScrollSpeed, time)@>
+    if (getTexMapArray()._materialParams.y != 1.0 && clamp(<$texcoord0$>, vec2(0.0), vec2(1.0)) != <$texcoord0$>) {
+        discard;
+    }
+
+    vec2 texCoord = <$texcoord0$>;
+
+<@if uvScrollSpeed and time@>
+    if ((<$matKey$> & UV_ANIMATION_SCROLL_VAL_BIT) != 0) {
+        <$uvScrollSpeed$> *= mix(1.0, fetchUVAnimationMaskMap(texCoord), float((<$matKey$> & UV_ANIMATION_MASK_MAP_BIT) != 0));
+        <$uvScrollSpeed$> *= time;
+        float cosTime = cos(<$uvScrollSpeed$>.z);
+        float sinTime = sin(<$uvScrollSpeed$>.z);
+        texCoord = (mat3(cosTime, sinTime, 0, -sinTime, cosTime, 0, 0, 0, 1) * vec3(texCoord - vec2(0.5), 1.0)).xy + vec2(0.5) + <$uvScrollSpeed$>.xy;
+    }
+<@endif@>
+
+<@if albedo@>
+    vec4 <$albedo$> = mix(vec4(1.0), fetchAlbedoMap(texCoord), float((<$matKey$> & (ALBEDO_MAP_BIT | OPACITY_MASK_MAP_BIT | OPACITY_TRANSLUCENT_MAP_BIT)) != 0));
+<@endif@>
+<@if normal@>
+    vec3 <$normal$> = mix(vec3(0.0, 1.0, 0.0), fetchNormalMap(texCoord), float((<$matKey$> & NORMAL_MAP_BIT) != 0));
+<@endif@>
+<@if shade@>
+    vec3 <$shade$> = float((<$matKey$> & SHADE_MAP_BIT) != 0) * fetchShadeMap(texCoord);
+<@endif@>
+<@if emissive@>
+    vec3 <$emissive$> = float((<$matKey$> & EMISSIVE_MAP_BIT) != 0) * fetchEmissiveMap(texCoord);
+<@endif@>
+<@if shadingShift@>
+    float <$shadingShift$> = float((<$matKey$> & SHADING_SHIFT_MAP_BIT) != 0) * fetchShadingShiftMap(texCoord);
+<@endif@>
+<@if rim@>
+    vec3 <$rim$> = mix(vec3(1.0), fetchRimMap(texCoord), float((<$matKey$> & RIM_MAP_BIT) != 0));
+<@endif@>
+<@endfunc@>
+
+<@func evalMaterialShade(fetchedShade, materialShade, matKey, shade)@>
+{
+    <$shade$> = mix(vec3(1.0), <$materialShade$>, float((<$matKey$> & SHADE_VAL_BIT) != 0));
+    <$shade$> *= mix(vec3(1.0), <$fetchedShade$>.rgb, float((<$matKey$> & SHADE_MAP_BIT) != 0));
+}
+<@endfunc@>
+
+<@func evalMaterialShadingShift(fetchedShadingShift, materialShadingShift, matKey, shadingShift)@>
+{
+    <$shadingShift$> = mix(0.0, <$materialShadingShift$>, float((<$matKey$> & SHADING_SHIFT_VAL_BIT) != 0));
+    <$shadingShift$> += mix(0.0, <$fetchedShadingShift$>.r, float((<$matKey$> & SHADING_SHIFT_MAP_BIT) != 0));
+}
+<@endfunc@>
+
+<@func evalMaterialMatcap(texcoord0, materialMatcap, matKey, matcap)@>
+{
+    if ((<$matKey$> & (MATCAP_VAL_BIT | MATCAP_MAP_BIT)) == 0) {
+        <$matcap$> = vec3(0.0);
+    } else {
+        <$matcap$> = mix(vec3(1.0), <$materialMatcap$>, float((<$matKey$> & MATCAP_VAL_BIT) != 0));
+        <$matcap$> *= mix(vec3(1.0), fetchMatcapMap(<$texcoord0$>), float((<$matKey$> & MATCAP_MAP_BIT) != 0));
+    }
+}
+<@endfunc@>
+
+<@func evalMaterialUVScrollSpeed(fetchedUVScrollMask, materialUVScrollMask, matKey, uvScrollSpeed)@>
+{
+    <$uvScrollSpeed$> = mix(vec3(1.0), <$materialUVScrollMask$>, float((<$matKey$> & UV_ANIMATION_MASK_MAP_BIT) != 0));
+    <$uvScrollSpeed$> *= mix(1.0, <$fetchedUVScrollMask$>.r, float((<$matKey$> & UV_ANIMATION_MASK_MAP_BIT) != 0));
+}
+<@endfunc@>
+<@endif@>
+
+<@endif@>
diff --git a/libraries/graphics/src/graphics/ShaderConstants.h b/libraries/graphics/src/graphics/ShaderConstants.h
index 8fd0df31f0..75eb4d00cc 100644
--- a/libraries/graphics/src/graphics/ShaderConstants.h
+++ b/libraries/graphics/src/graphics/ShaderConstants.h
@@ -1,6 +1,7 @@
 // <!
 //  Created by Bradley Austin Davis on 2018/05/25
 //  Copyright 2013-2018 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -29,6 +30,13 @@
 #define GRAPHICS_TEXTURE_MATERIAL_SCATTERING 6
 #define GRAPHICS_TEXTURE_MATERIAL_MIRROR 1 // Mirrors use albedo textures, but nothing else
 
+// Keep aligned with procedural/ProceduralMaterialCache.h
+#define GRAPHICS_TEXTURE_MATERIAL_SHADE GRAPHICS_TEXTURE_MATERIAL_METALLIC
+#define GRAPHICS_TEXTURE_MATERIAL_SHADING_SHIFT GRAPHICS_TEXTURE_MATERIAL_ROUGHNESS
+#define GRAPHICS_TEXTURE_MATERIAL_MATCAP GRAPHICS_TEXTURE_MATERIAL_OCCLUSION
+#define GRAPHICS_TEXTURE_MATERIAL_RIM GRAPHICS_TEXTURE_MATERIAL_SCATTERING
+#define GRAPHICS_TEXTURE_MATERIAL_UV_ANIMATION_MASK 7
+
 // Make sure these match the ones in render-utils/ShaderConstants.h
 #define GRAPHICS_TEXTURE_SKYBOX 11
 #define GRAPHICS_BUFFER_SKYBOX_PARAMS 5
@@ -61,7 +69,13 @@ enum Texture {
     MaterialOcclusion = GRAPHICS_TEXTURE_MATERIAL_OCCLUSION,
     MaterialScattering = GRAPHICS_TEXTURE_MATERIAL_SCATTERING,
     MaterialMirror = GRAPHICS_TEXTURE_MATERIAL_MIRROR,
-    Skybox = GRAPHICS_TEXTURE_SKYBOX
+    Skybox = GRAPHICS_TEXTURE_SKYBOX,
+
+    MaterialShade = GRAPHICS_TEXTURE_MATERIAL_SHADE,
+    MaterialShadingShift = GRAPHICS_TEXTURE_MATERIAL_SHADING_SHIFT,
+    MaterialMatcap = GRAPHICS_TEXTURE_MATERIAL_MATCAP,
+    MaterialRim = GRAPHICS_TEXTURE_MATERIAL_RIM,
+    MaterialUVAnimationMask = GRAPHICS_TEXTURE_MATERIAL_UV_ANIMATION_MASK,
 };
 } // namespace texture
 
diff --git a/libraries/hfm/src/hfm/HFM.cpp b/libraries/hfm/src/hfm/HFM.cpp
index dd13d6d4f3..53c3ec18dd 100644
--- a/libraries/hfm/src/hfm/HFM.cpp
+++ b/libraries/hfm/src/hfm/HFM.cpp
@@ -47,6 +47,24 @@ void HFMMaterial::getTextureNames(QSet<QString>& textureList) const {
     if (!lightmapTexture.isNull()) {
         textureList.insert(lightmapTexture.name);
     }
+
+    if (isMToonMaterial) {
+        if (!shadeTexture.isNull()) {
+            textureList.insert(shadeTexture.name);
+        }
+        if (!shadingShiftTexture.isNull()) {
+            textureList.insert(shadingShiftTexture.name);
+        }
+        if (!matcapTexture.isNull()) {
+            textureList.insert(matcapTexture.name);
+        }
+        if (!rimTexture.isNull()) {
+            textureList.insert(rimTexture.name);
+        }
+        if (!uvAnimationTexture.isNull()) {
+            textureList.insert(uvAnimationTexture.name);
+        }
+    }
 }
 
 void HFMMaterial::setMaxNumPixelsPerTexture(int maxNumPixels) {
@@ -61,6 +79,12 @@ void HFMMaterial::setMaxNumPixelsPerTexture(int maxNumPixels) {
     occlusionTexture.maxNumPixels = maxNumPixels;
     scatteringTexture.maxNumPixels = maxNumPixels;
     lightmapTexture.maxNumPixels = maxNumPixels;
+
+    shadeTexture.maxNumPixels = maxNumPixels;
+    shadingShiftTexture.maxNumPixels = maxNumPixels;
+    matcapTexture.maxNumPixels = maxNumPixels;
+    rimTexture.maxNumPixels = maxNumPixels;
+    uvAnimationTexture.maxNumPixels = maxNumPixels;
 }
 
 bool HFMMaterial::needTangentSpace() const {
@@ -312,6 +336,13 @@ void HFMModel::debugDump() {
         qCDebug(modelformat) << "  useMetallicMap =" << mat.useMetallicMap;
         qCDebug(modelformat) << "  useEmissiveMap =" << mat.useEmissiveMap;
         qCDebug(modelformat) << "  useOcclusionMap =" << mat.useOcclusionMap;
+
+        qCDebug(modelformat) << "  isMToonMaterial =" << mat.isMToonMaterial;
+        qCDebug(modelformat) << "  shadeTexture =" << mat.shadeTexture.filename;
+        qCDebug(modelformat) << "  shadingShiftTexture =" << mat.shadingShiftTexture.filename;
+        qCDebug(modelformat) << "  matcapTexture =" << mat.matcapTexture.filename;
+        qCDebug(modelformat) << "  rimTexture =" << mat.rimTexture.filename;
+        qCDebug(modelformat) << "  uvAnimationTexture =" << mat.uvAnimationTexture.filename;
     }
 
     qCDebug(modelformat) << "---------------- Joints ----------------";
diff --git a/libraries/hfm/src/hfm/HFM.h b/libraries/hfm/src/hfm/HFM.h
index d082f30dc5..6932572b1d 100644
--- a/libraries/hfm/src/hfm/HFM.h
+++ b/libraries/hfm/src/hfm/HFM.h
@@ -224,6 +224,14 @@ public:
     bool useEmissiveMap { false };
     bool useOcclusionMap { false };
 
+
+    bool isMToonMaterial { false };
+    Texture shadeTexture;
+    Texture shadingShiftTexture;
+    Texture matcapTexture;
+    Texture rimTexture;
+    Texture uvAnimationTexture;
+
     bool needTangentSpace() const;
 };
 
diff --git a/libraries/image/src/image/OpenEXRReader.cpp b/libraries/image/src/image/OpenEXRReader.cpp
index 66e304e3fa..d1af1c9621 100644
--- a/libraries/image/src/image/OpenEXRReader.cpp
+++ b/libraries/image/src/image/OpenEXRReader.cpp
@@ -23,6 +23,7 @@
 #include <OpenEXR/ImfRgbaFile.h>
 #include <OpenEXR/ImfArray.h>
 #include <OpenEXR/ImfTestFile.h>
+#include <OpenEXR/ImfInt64.h>
 
 class QIODeviceImfStream : public Imf::IStream {
 public:
@@ -39,11 +40,11 @@ public:
         return true;
     }
 
-    Imf::Int64 tellg() override {
+    uint64_t tellg() override {
         return _device.pos();
     }
 
-    void seekg(Imf::Int64 pos) override {
+    void seekg(uint64_t pos) override {
         _device.seek(pos);
     }
 
@@ -76,7 +77,7 @@ image::Image image::readOpenEXR(QIODevice& content, const std::string& filename)
 
         Image image{ width, height, Image::Format_PACKED_FLOAT };
         auto packHDRPixel = getHDRPackingFunction();
-        
+
         for (int y = 0; y < height; y++) {
             const auto srcScanline = pixels[y];
             gpu::uint32* dstScanline = (gpu::uint32*) image.editScanLine(y);
diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp
index 738c61874f..ddfdeb79d1 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.cpp
+++ b/libraries/model-networking/src/model-networking/ModelCache.cpp
@@ -325,7 +325,11 @@ void GeometryResource::setGeometryDefinition(HFMModel::Pointer hfmModel, const M
     QHash<QString, size_t> materialIDAtlas;
     for (const HFMMaterial& material : _hfmModel->materials) {
         materialIDAtlas[material.materialID] = _materials.size();
-        _materials.push_back(std::make_shared<NetworkMaterial>(material, _textureBaseURL));
+        if (!material.isMToonMaterial) {
+            _materials.push_back(std::make_shared<NetworkMaterial>(material, _textureBaseURL));
+        } else {
+            _materials.push_back(std::make_shared<NetworkMToonMaterial>(material, _textureBaseURL));
+        }
     }
 
     std::shared_ptr<GeometryMeshes> meshes = std::make_shared<GeometryMeshes>();
@@ -355,8 +359,16 @@ void GeometryResource::deleter() {
 
 void GeometryResource::setTextures() {
     if (_hfmModel) {
-        for (const HFMMaterial& material : _hfmModel->materials) {
-            _materials.push_back(std::make_shared<NetworkMaterial>(material, _textureBaseURL));
+        if (DependencyManager::get<TextureCache>()) {
+            for (const HFMMaterial& material : _hfmModel->materials) {
+                if (!material.isMToonMaterial) {
+                    _materials.push_back(std::make_shared<NetworkMaterial>(material, _textureBaseURL));
+                } else {
+                    _materials.push_back(std::make_shared<NetworkMToonMaterial>(material, _textureBaseURL));
+                }
+            }
+        } else {
+            qDebug() << "GeometryResource::setTextures: TextureCache dependency not available, skipping textures";
         }
     }
 }
@@ -432,7 +444,11 @@ Geometry::Geometry(const Geometry& geometry) {
 
     _materials.reserve(geometry._materials.size());
     for (const auto& material : geometry._materials) {
-        _materials.push_back(std::make_shared<NetworkMaterial>(*material));
+        if (!material->isMToon()) {
+            _materials.push_back(std::make_shared<NetworkMaterial>(*material));
+        } else if (auto mToonMaterial = std::static_pointer_cast<NetworkMToonMaterial>(material)) {
+            _materials.push_back(std::make_shared<NetworkMToonMaterial>(*mToonMaterial));
+        }
     }
 
     _animGraphOverrideUrl = geometry._animGraphOverrideUrl;
@@ -448,9 +464,13 @@ void Geometry::setTextures(const QVariantMap& textureMap) {
 
                 // FIXME: The Model currently caches the materials (waste of space!)
                 //        so they must be copied in the Geometry copy-ctor
-                // if (material->isOriginal()) {
+                //if (material->isOriginal()) {
                 //    // Copy the material to avoid mutating the cached version
-                //    material = std::make_shared<NetworkMaterial>(*material);
+                //    if (!material->isMToon()) {
+                //        material = std::make_shared<NetworkMaterial>(*material);
+                //    } else {
+                //        material = std::make_shared<NetworkMToonMaterial>(*material);
+                //    }
                 //}
 
                 material->setTextures(textureMap);
diff --git a/libraries/model-serializers/CMakeLists.txt b/libraries/model-serializers/CMakeLists.txt
index 76775896dc..b0b0dd6344 100644
--- a/libraries/model-serializers/CMakeLists.txt
+++ b/libraries/model-serializers/CMakeLists.txt
@@ -1,7 +1,7 @@
 set(TARGET_NAME model-serializers)
 setup_hifi_library()
 
-link_hifi_libraries(shared graphics networking image hfm)
+link_hifi_libraries(shared graphics networking image hfm procedural material-networking ktx shaders)
 include_hifi_library_headers(gpu image)
 
 target_draco()
diff --git a/libraries/model-serializers/src/GLTFSerializer.cpp b/libraries/model-serializers/src/GLTFSerializer.cpp
index 488a59d7cb..1440fb22d2 100644
--- a/libraries/model-serializers/src/GLTFSerializer.cpp
+++ b/libraries/model-serializers/src/GLTFSerializer.cpp
@@ -10,6 +10,8 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
 
+#define CGLTF_IMPLEMENTATION
+
 #include "GLTFSerializer.h"
 
 #include <QtCore/QBuffer>
@@ -28,15 +30,32 @@
 #include <qfile.h>
 #include <qfileinfo.h>
 
+#include <sstream>
+
+#include <glm/gtx/transform.hpp>
+
 #include <shared/NsightHelpers.h>
 #include <NetworkAccessManager.h>
 #include <ResourceManager.h>
 #include <PathUtils.h>
 #include <image/ColorChannel.h>
 #include <BlendshapeConstants.h>
+#include <procedural/ProceduralMaterialCache.h>
 
 #include "FBXSerializer.h"
 
+float atof_locale_independent(char* str) {
+    //TODO: Once we have C++17 we can use std::from_chars
+    std::istringstream streamToParse(str);
+    streamToParse.imbue(std::locale("C"));
+    float value;
+    if (!(streamToParse >> value)) {
+        qDebug(modelformat) << "cgltf: Cannot parse float from string: " << str;
+        return 0.0f;
+    }
+    return value;
+}
+
 #define GLTF_GET_INDICIES(accCount) int index1 = (indices[n + 0] * accCount); int index2 = (indices[n + 1] * accCount); int index3 = (indices[n + 2] * accCount);
 
 #define GLTF_APPEND_ARRAY_1(newArray, oldArray) GLTF_GET_INDICIES(1) \
@@ -59,749 +78,30 @@ newArray.append(oldArray[index1]); newArray.append(oldArray[index1 + 1]); newArr
 newArray.append(oldArray[index2]); newArray.append(oldArray[index2 + 1]); newArray.append(oldArray[index2 + 2]); newArray.append(oldArray[index2 + 3]); \
 newArray.append(oldArray[index3]); newArray.append(oldArray[index3 + 1]); newArray.append(oldArray[index3 + 2]); newArray.append(oldArray[index3 + 3]);
 
-bool GLTFSerializer::getStringVal(const QJsonObject& object, const QString& fieldname,
-                              QString& value, QMap<QString, bool>&  defined) {
-    bool _defined = (object.contains(fieldname) && object[fieldname].isString());
-    if (_defined) {
-        value = object[fieldname].toString();
-    }
-    defined.insert(fieldname, _defined);
-    return _defined;
-}
-
-bool GLTFSerializer::getBoolVal(const QJsonObject& object, const QString& fieldname,
-                            bool& value, QMap<QString, bool>&  defined) {
-    bool _defined = (object.contains(fieldname) && object[fieldname].isBool());
-    if (_defined) {
-        value = object[fieldname].toBool();
-    }
-    defined.insert(fieldname, _defined);
-    return _defined;
-}
-
-bool GLTFSerializer::getIntVal(const QJsonObject& object, const QString& fieldname,
-                           int& value, QMap<QString, bool>&  defined) {
-    bool _defined = (object.contains(fieldname) && !object[fieldname].isNull());
-    if (_defined) {
-        value = object[fieldname].toInt();
-    }
-    defined.insert(fieldname, _defined);
-    return _defined;
-}
-
-bool GLTFSerializer::getDoubleVal(const QJsonObject& object, const QString& fieldname,
-                              double& value, QMap<QString, bool>&  defined) {
-    bool _defined = (object.contains(fieldname) && object[fieldname].isDouble());
-    if (_defined) {
-        value = object[fieldname].toDouble();
-    }
-    defined.insert(fieldname, _defined);
-    return _defined;
-}
-bool GLTFSerializer::getObjectVal(const QJsonObject& object, const QString& fieldname,
-                              QJsonObject& value, QMap<QString, bool>&  defined) {
-    bool _defined = (object.contains(fieldname) && object[fieldname].isObject());
-    if (_defined) {
-        value = object[fieldname].toObject();
-    }
-    defined.insert(fieldname, _defined);
-    return _defined;
-}
-
-bool GLTFSerializer::getIntArrayVal(const QJsonObject& object, const QString& fieldname,
-                                QVector<int>& values, QMap<QString, bool>&  defined) {
-    bool _defined = (object.contains(fieldname) && object[fieldname].isArray());
-    if (_defined) {
-        QJsonArray arr = object[fieldname].toArray();
-        foreach(const QJsonValue & v, arr) {
-            if (!v.isNull()) {
-                values.push_back(v.toInt());
-            }
-        }
-    }
-    defined.insert(fieldname, _defined);
-    return _defined;
-}
-
-bool GLTFSerializer::getDoubleArrayVal(const QJsonObject& object, const QString& fieldname,
-                                   QVector<double>& values, QMap<QString, bool>&  defined) {
-    bool _defined = (object.contains(fieldname) && object[fieldname].isArray());
-    if (_defined) {
-        QJsonArray arr = object[fieldname].toArray();
-        foreach(const QJsonValue & v, arr) {
-            if (v.isDouble()) {
-                values.push_back(v.toDouble());
-            }
-        }
-    }
-    defined.insert(fieldname, _defined);
-    return _defined;
-}
-
-bool GLTFSerializer::getObjectArrayVal(const QJsonObject& object, const QString& fieldname,
-                                   QJsonArray& objects, QMap<QString, bool>& defined) {
-    bool _defined = (object.contains(fieldname) && object[fieldname].isArray());
-    if (_defined) {
-        objects = object[fieldname].toArray();
-    }
-    defined.insert(fieldname, _defined);
-    return _defined;
-}
-
-hifi::ByteArray GLTFSerializer::setGLBChunks(const hifi::ByteArray& data) {
-    int byte = 4;
-    int jsonStart = data.indexOf("JSON", Qt::CaseSensitive);
-    int binStart = data.indexOf("BIN", Qt::CaseSensitive);
-    int jsonLength, binLength;
-    hifi::ByteArray jsonLengthChunk, binLengthChunk;
-
-    jsonLengthChunk = data.mid(jsonStart - byte, byte);
-    QDataStream tempJsonLen(jsonLengthChunk);
-    tempJsonLen.setByteOrder(QDataStream::LittleEndian);
-    tempJsonLen >> jsonLength;
-    hifi::ByteArray jsonChunk = data.mid(jsonStart + byte, jsonLength);
-
-    if (binStart != -1) {
-        binLengthChunk = data.mid(binStart - byte, byte);
-
-        QDataStream tempBinLen(binLengthChunk);
-        tempBinLen.setByteOrder(QDataStream::LittleEndian);
-        tempBinLen >> binLength;
-
-        _glbBinary = data.mid(binStart + byte, binLength);
-    }
-    return jsonChunk;
-}
-
-int GLTFSerializer::getMeshPrimitiveRenderingMode(const QString& type)
-{
-    if (type == "POINTS") {
-        return GLTFMeshPrimitivesRenderingMode::POINTS;
-    }
-    if (type == "LINES") {
-        return GLTFMeshPrimitivesRenderingMode::LINES;
-    }
-    if (type == "LINE_LOOP") {
-        return GLTFMeshPrimitivesRenderingMode::LINE_LOOP;
-    }
-    if (type == "LINE_STRIP") {
-        return GLTFMeshPrimitivesRenderingMode::LINE_STRIP;
-    }
-    if (type == "TRIANGLES") {
-        return GLTFMeshPrimitivesRenderingMode::TRIANGLES;
-    }
-    if (type == "TRIANGLE_STRIP") {
-        return GLTFMeshPrimitivesRenderingMode::TRIANGLE_STRIP;
-    }
-    if (type == "TRIANGLE_FAN") {
-        return GLTFMeshPrimitivesRenderingMode::TRIANGLE_FAN;
-    }
-    return GLTFMeshPrimitivesRenderingMode::TRIANGLES;
-}
-
-int GLTFSerializer::getAccessorType(const QString& type)
-{
-    if (type == "SCALAR") {
-        return GLTFAccessorType::SCALAR;
-    }
-    if (type == "VEC2") {
-        return GLTFAccessorType::VEC2;
-    }
-    if (type == "VEC3") {
-        return GLTFAccessorType::VEC3;
-    }
-    if (type == "VEC4") {
-        return GLTFAccessorType::VEC4;
-    }
-    if (type == "MAT2") {
-        return GLTFAccessorType::MAT2;
-    }
-    if (type == "MAT3") {
-        return GLTFAccessorType::MAT3;
-    }
-    if (type == "MAT4") {
-        return GLTFAccessorType::MAT4;
-    }
-    return GLTFAccessorType::SCALAR;
-}
-
-graphics::MaterialKey::OpacityMapMode GLTFSerializer::getMaterialAlphaMode(const QString& type) {
-    if (type == "OPAQUE") {
-        return graphics::MaterialKey::OPACITY_MAP_OPAQUE;
-    }
-    if (type == "MASK") {
-        return graphics::MaterialKey::OPACITY_MAP_MASK;
-    }
-    if (type == "BLEND") {
-        return graphics::MaterialKey::OPACITY_MAP_BLEND;
-    }
-    return graphics::MaterialKey::OPACITY_MAP_BLEND;
-}
-
-int GLTFSerializer::getCameraType(const QString& type)
-{
-    if (type == "orthographic") {
-        return GLTFCameraTypes::ORTHOGRAPHIC;
-    }
-    if (type == "perspective") {
-        return GLTFCameraTypes::PERSPECTIVE;
-    }
-    return GLTFCameraTypes::PERSPECTIVE;
-}
-
-int GLTFSerializer::getImageMimeType(const QString& mime)
-{
-    if (mime == "image/jpeg") {
-        return GLTFImageMimetype::JPEG;
-    }
-    if (mime == "image/png") {
-        return GLTFImageMimetype::PNG;
-    }
-    return GLTFImageMimetype::JPEG;
-}
-
-int GLTFSerializer::getAnimationSamplerInterpolation(const QString& interpolation)
-{
-    if (interpolation == "LINEAR") {
-        return GLTFAnimationSamplerInterpolation::LINEAR;
-    }
-    return GLTFAnimationSamplerInterpolation::LINEAR;
-}
-
-bool GLTFSerializer::setAsset(const QJsonObject& object) {
-    QJsonObject jsAsset;
-    bool isAssetDefined = getObjectVal(object, "asset", jsAsset, _file.defined);
-    if (isAssetDefined) {
-        if (!getStringVal(jsAsset, "version", _file.asset.version,
-                          _file.asset.defined) || _file.asset.version != "2.0") {
-            return false;
-        }
-        getStringVal(jsAsset, "generator", _file.asset.generator, _file.asset.defined);
-        getStringVal(jsAsset, "copyright", _file.asset.copyright, _file.asset.defined);
-    }
-    return isAssetDefined;
-}
-
-GLTFAccessor::GLTFAccessorSparse::GLTFAccessorSparseIndices GLTFSerializer::createAccessorSparseIndices(const QJsonObject& object) {
-    GLTFAccessor::GLTFAccessorSparse::GLTFAccessorSparseIndices accessorSparseIndices;
-
-    getIntVal(object, "bufferView", accessorSparseIndices.bufferView, accessorSparseIndices.defined);
-    getIntVal(object, "byteOffset", accessorSparseIndices.byteOffset, accessorSparseIndices.defined);
-    getIntVal(object, "componentType", accessorSparseIndices.componentType, accessorSparseIndices.defined);
-
-    return accessorSparseIndices;
-}
-
-GLTFAccessor::GLTFAccessorSparse::GLTFAccessorSparseValues GLTFSerializer::createAccessorSparseValues(const QJsonObject& object) {
-    GLTFAccessor::GLTFAccessorSparse::GLTFAccessorSparseValues accessorSparseValues;
-
-    getIntVal(object, "bufferView", accessorSparseValues.bufferView, accessorSparseValues.defined);
-    getIntVal(object, "byteOffset", accessorSparseValues.byteOffset, accessorSparseValues.defined);
-
-    return accessorSparseValues;
-}
-
-GLTFAccessor::GLTFAccessorSparse GLTFSerializer::createAccessorSparse(const QJsonObject& object) {
-    GLTFAccessor::GLTFAccessorSparse accessorSparse;
-
-    getIntVal(object, "count", accessorSparse.count, accessorSparse.defined);
-    QJsonObject sparseIndicesObject;
-    if (getObjectVal(object, "indices", sparseIndicesObject, accessorSparse.defined)) {
-        accessorSparse.indices = createAccessorSparseIndices(sparseIndicesObject);
-    }
-    QJsonObject sparseValuesObject;
-    if (getObjectVal(object, "values", sparseValuesObject, accessorSparse.defined)) {
-        accessorSparse.values = createAccessorSparseValues(sparseValuesObject);
-    }
-
-    return accessorSparse;
-}
-
-bool GLTFSerializer::addAccessor(const QJsonObject& object) {
-    GLTFAccessor accessor;
-
-    getIntVal(object, "bufferView", accessor.bufferView, accessor.defined);
-    getIntVal(object, "byteOffset", accessor.byteOffset, accessor.defined);
-    getIntVal(object, "componentType", accessor.componentType, accessor.defined);
-    getIntVal(object, "count", accessor.count, accessor.defined);
-    getBoolVal(object, "normalized", accessor.normalized, accessor.defined);
-    QString type;
-    if (getStringVal(object, "type", type, accessor.defined)) {
-        accessor.type = getAccessorType(type);
-    }
-
-    QJsonObject sparseObject;
-    if (getObjectVal(object, "sparse", sparseObject, accessor.defined)) {
-        accessor.sparse = createAccessorSparse(sparseObject);
-    }
-
-    getDoubleArrayVal(object, "max", accessor.max, accessor.defined);
-    getDoubleArrayVal(object, "min", accessor.min, accessor.defined);
-
-    _file.accessors.push_back(accessor);
-
-    return true;
-}
-
-bool GLTFSerializer::addAnimation(const QJsonObject& object) {
-    GLTFAnimation animation;
-
-    QJsonArray channels;
-    if (getObjectArrayVal(object, "channels", channels, animation.defined)) {
-        foreach(const QJsonValue & v, channels) {
-            if (v.isObject()) {
-                GLTFChannel channel;
-                getIntVal(v.toObject(), "sampler", channel.sampler, channel.defined);
-                QJsonObject jsChannel;
-                if (getObjectVal(v.toObject(), "target", jsChannel, channel.defined)) {
-                    getIntVal(jsChannel, "node", channel.target.node, channel.target.defined);
-                    getIntVal(jsChannel, "path", channel.target.path, channel.target.defined);
-                }
-            }
-        }
-    }
-
-    QJsonArray samplers;
-    if (getObjectArrayVal(object, "samplers", samplers, animation.defined)) {
-        foreach(const QJsonValue & v, samplers) {
-            if (v.isObject()) {
-                GLTFAnimationSampler sampler;
-                getIntVal(v.toObject(), "input", sampler.input, sampler.defined);
-                getIntVal(v.toObject(), "output", sampler.input, sampler.defined);
-                QString interpolation;
-                if (getStringVal(v.toObject(), "interpolation", interpolation, sampler.defined)) {
-                    sampler.interpolation = getAnimationSamplerInterpolation(interpolation);
-                }
-            }
-        }
-    }
-
-    _file.animations.push_back(animation);
-
-    return true;
-}
-
-bool GLTFSerializer::addBufferView(const QJsonObject& object) {
-    GLTFBufferView bufferview;
-
-    getIntVal(object, "buffer", bufferview.buffer, bufferview.defined);
-    getIntVal(object, "byteLength", bufferview.byteLength, bufferview.defined);
-    getIntVal(object, "byteOffset", bufferview.byteOffset, bufferview.defined);
-    getIntVal(object, "target", bufferview.target, bufferview.defined);
-
-    _file.bufferviews.push_back(bufferview);
-
-    return true;
-}
-
-bool GLTFSerializer::addBuffer(const QJsonObject& object) {
-    GLTFBuffer buffer;
-
-    getIntVal(object, "byteLength", buffer.byteLength, buffer.defined);
-
-    if (_url.path().endsWith("glb")) {
-        if (!_glbBinary.isEmpty()) {
-            buffer.blob = _glbBinary;
-        } else {
-            return false;
-        }
-    }
-    if (getStringVal(object, "uri", buffer.uri, buffer.defined)) {
-        if (!readBinary(buffer.uri, buffer.blob)) {
-            return false;
-        }
-    }
-    _file.buffers.push_back(buffer);
-
-    return true;
-}
-
-bool GLTFSerializer::addCamera(const QJsonObject& object) {
-    GLTFCamera camera;
-
-    QJsonObject jsPerspective;
-    QJsonObject jsOrthographic;
-    QString type;
-    getStringVal(object, "name", camera.name, camera.defined);
-    if (getObjectVal(object, "perspective", jsPerspective, camera.defined)) {
-        getDoubleVal(jsPerspective, "aspectRatio", camera.perspective.aspectRatio, camera.perspective.defined);
-        getDoubleVal(jsPerspective, "yfov", camera.perspective.yfov, camera.perspective.defined);
-        getDoubleVal(jsPerspective, "zfar", camera.perspective.zfar, camera.perspective.defined);
-        getDoubleVal(jsPerspective, "znear", camera.perspective.znear, camera.perspective.defined);
-        camera.type = GLTFCameraTypes::PERSPECTIVE;
-    } else if (getObjectVal(object, "orthographic", jsOrthographic, camera.defined)) {
-        getDoubleVal(jsOrthographic, "zfar", camera.orthographic.zfar, camera.orthographic.defined);
-        getDoubleVal(jsOrthographic, "znear", camera.orthographic.znear, camera.orthographic.defined);
-        getDoubleVal(jsOrthographic, "xmag", camera.orthographic.xmag, camera.orthographic.defined);
-        getDoubleVal(jsOrthographic, "ymag", camera.orthographic.ymag, camera.orthographic.defined);
-        camera.type = GLTFCameraTypes::ORTHOGRAPHIC;
-    } else if (getStringVal(object, "type", type, camera.defined)) {
-        camera.type = getCameraType(type);
-    }
-
-    _file.cameras.push_back(camera);
-
-    return true;
-}
-
-bool GLTFSerializer::addImage(const QJsonObject& object) {
-    GLTFImage image;
-
-    QString mime;
-    getStringVal(object, "uri", image.uri, image.defined);
-    if (image.uri.contains("data:image/png;base64,")) {
-        image.mimeType = getImageMimeType("image/png");
-    } else if (image.uri.contains("data:image/jpeg;base64,")) {
-        image.mimeType = getImageMimeType("image/jpeg");
-    }
-    if (getStringVal(object, "mimeType", mime, image.defined)) {
-        image.mimeType = getImageMimeType(mime);
-    }
-    getIntVal(object, "bufferView", image.bufferView, image.defined);
-
-    _file.images.push_back(image);
-
-    return true;
-}
-
-bool GLTFSerializer::getIndexFromObject(const QJsonObject& object, const QString& field,
-                                    int& outidx, QMap<QString, bool>& defined) {
-    QJsonObject subobject;
-    if (getObjectVal(object, field, subobject, defined)) {
-        QMap<QString, bool> tmpdefined = QMap<QString, bool>();
-        return getIntVal(subobject, "index", outidx, tmpdefined);
-    }
-    return false;
-}
-
-bool GLTFSerializer::addMaterial(const QJsonObject& object) {
-    GLTFMaterial material;
-
-    getStringVal(object, "name", material.name, material.defined);
-    getDoubleArrayVal(object, "emissiveFactor", material.emissiveFactor, material.defined);
-    getIndexFromObject(object, "emissiveTexture", material.emissiveTexture, material.defined);
-    getIndexFromObject(object, "normalTexture", material.normalTexture, material.defined);
-    getIndexFromObject(object, "occlusionTexture", material.occlusionTexture, material.defined);
-    getBoolVal(object, "doubleSided", material.doubleSided, material.defined);
-    QString alphaMode;
-    if (getStringVal(object, "alphaMode", alphaMode, material.defined)) {
-        material.alphaMode = getMaterialAlphaMode(alphaMode);
-    }
-    getDoubleVal(object, "alphaCutoff", material.alphaCutoff, material.defined);
-    QJsonObject jsMetallicRoughness;
-    if (getObjectVal(object, "pbrMetallicRoughness", jsMetallicRoughness, material.defined)) {
-        getDoubleArrayVal(jsMetallicRoughness, "baseColorFactor",
-                          material.pbrMetallicRoughness.baseColorFactor,
-                          material.pbrMetallicRoughness.defined);
-        getIndexFromObject(jsMetallicRoughness, "baseColorTexture",
-                           material.pbrMetallicRoughness.baseColorTexture,
-                           material.pbrMetallicRoughness.defined);
-        // Undefined metallicFactor used with pbrMetallicRoughness means metallicFactor == 1.0
-        if (!getDoubleVal(jsMetallicRoughness, "metallicFactor",
-                     material.pbrMetallicRoughness.metallicFactor,
-                     material.pbrMetallicRoughness.defined)) {
-            material.pbrMetallicRoughness.metallicFactor = 1.0;
-            material.pbrMetallicRoughness.defined["metallicFactor"] = true;
-        }
-        getDoubleVal(jsMetallicRoughness, "roughnessFactor",
-                     material.pbrMetallicRoughness.roughnessFactor,
-                     material.pbrMetallicRoughness.defined);
-        getIndexFromObject(jsMetallicRoughness, "metallicRoughnessTexture",
-                           material.pbrMetallicRoughness.metallicRoughnessTexture,
-                           material.pbrMetallicRoughness.defined);
-    }
-   _file.materials.push_back(material);
-    return true;
-}
-
-bool GLTFSerializer::addMesh(const QJsonObject& object) {
-    GLTFMesh mesh;
-
-    getStringVal(object, "name", mesh.name, mesh.defined);
-    getDoubleArrayVal(object, "weights", mesh.weights, mesh.defined);
-    QJsonArray jsPrimitives;
-    object.keys();
-    if (getObjectArrayVal(object, "primitives", jsPrimitives, mesh.defined)) {
-        foreach(const QJsonValue & prim, jsPrimitives) {
-            if (prim.isObject()) {
-                GLTFMeshPrimitive primitive;
-                QJsonObject jsPrimitive = prim.toObject();
-                getIntVal(jsPrimitive, "mode", primitive.mode, primitive.defined);
-                getIntVal(jsPrimitive, "indices", primitive.indices, primitive.defined);
-                getIntVal(jsPrimitive, "material", primitive.material, primitive.defined);
-
-                QJsonObject jsAttributes;
-                if (getObjectVal(jsPrimitive, "attributes", jsAttributes, primitive.defined)) {
-                    QStringList attrKeys = jsAttributes.keys();
-                    foreach(const QString & attrKey, attrKeys) {
-                        int attrVal;
-                        getIntVal(jsAttributes, attrKey, attrVal, primitive.attributes.defined);
-                        primitive.attributes.values.insert(attrKey, attrVal);
-                    }
-                }
-
-                QJsonArray jsTargets;
-                if (getObjectArrayVal(jsPrimitive, "targets", jsTargets, primitive.defined)) {
-                    foreach(const QJsonValue & tar, jsTargets) {
-                        if (tar.isObject()) {
-                            QJsonObject jsTarget = tar.toObject();
-                            QStringList tarKeys = jsTarget.keys();
-                            GLTFMeshPrimitiveAttr target;
-                            foreach(const QString & tarKey, tarKeys) {
-                                int tarVal;
-                                getIntVal(jsTarget, tarKey, tarVal, target.defined);
-                                target.values.insert(tarKey, tarVal);
-                            }
-                            primitive.targets.push_back(target);
-                        }
-                    }
-                }
-                mesh.primitives.push_back(primitive);
-            }
-        }
-    }
-
-    QJsonObject jsExtras;
-    GLTFMeshExtra extras;
-    if (getObjectVal(object, "extras", jsExtras, mesh.defined)) {
-        QJsonArray jsTargetNames;
-        if (getObjectArrayVal(jsExtras, "targetNames", jsTargetNames, extras.defined)) {
-            foreach (const QJsonValue& tarName, jsTargetNames) {
-                extras.targetNames.push_back(tarName.toString());
-            }
-        }
-        mesh.extras = extras;
-    }
-
-    _file.meshes.push_back(mesh);
-
-    return true;
-}
-
-bool GLTFSerializer::addNode(const QJsonObject& object) {
-    GLTFNode node;
-
-    getStringVal(object, "name", node.name, node.defined);
-    getIntVal(object, "camera", node.camera, node.defined);
-    getIntVal(object, "mesh", node.mesh, node.defined);
-    getIntArrayVal(object, "children", node.children, node.defined);
-    getDoubleArrayVal(object, "translation", node.translation, node.defined);
-    getDoubleArrayVal(object, "rotation", node.rotation, node.defined);
-    getDoubleArrayVal(object, "scale", node.scale, node.defined);
-    getDoubleArrayVal(object, "matrix", node.matrix, node.defined);
-    getIntVal(object, "skin", node.skin, node.defined);
-    getStringVal(object, "jointName", node.jointName, node.defined);
-    getIntArrayVal(object, "skeletons", node.skeletons, node.defined);
-
-    _file.nodes.push_back(node);
-
-    return true;
-}
-
-bool GLTFSerializer::addSampler(const QJsonObject& object) {
-    GLTFSampler sampler;
-
-    getIntVal(object, "magFilter", sampler.magFilter, sampler.defined);
-    getIntVal(object, "minFilter", sampler.minFilter, sampler.defined);
-    getIntVal(object, "wrapS", sampler.wrapS, sampler.defined);
-    getIntVal(object, "wrapT", sampler.wrapT, sampler.defined);
-
-    _file.samplers.push_back(sampler);
-
-    return true;
-
-}
-
-bool GLTFSerializer::addScene(const QJsonObject& object) {
-    GLTFScene scene;
-
-    getStringVal(object, "name", scene.name, scene.defined);
-    getIntArrayVal(object, "nodes", scene.nodes, scene.defined);
-
-    _file.scenes.push_back(scene);
-    return true;
-}
-
-bool GLTFSerializer::addSkin(const QJsonObject& object) {
-    GLTFSkin skin;
-
-    getIntVal(object, "inverseBindMatrices", skin.inverseBindMatrices, skin.defined);
-    getIntVal(object, "skeleton", skin.skeleton, skin.defined);
-    getIntArrayVal(object, "joints", skin.joints, skin.defined);
-
-    _file.skins.push_back(skin);
-
-    return true;
-}
-
-bool GLTFSerializer::addTexture(const QJsonObject& object) {
-    GLTFTexture texture;
-    getIntVal(object, "sampler", texture.sampler, texture.defined);
-    getIntVal(object, "source", texture.source, texture.defined);
-
-    _file.textures.push_back(texture);
-
-    return true;
-}
-
-bool GLTFSerializer::parseGLTF(const hifi::ByteArray& data) {
-    PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr);
-
-    hifi::ByteArray jsonChunk = data;
-
-    if (_url.path().endsWith("glb") && data.indexOf("glTF") == 0 && data.contains("JSON")) {
-        jsonChunk = setGLBChunks(data);
-    }
-
-    QJsonDocument d = QJsonDocument::fromJson(jsonChunk);
-    QJsonObject jsFile = d.object();
-
-    bool success = setAsset(jsFile);
-    if (success) {
-        QJsonArray accessors;
-        if (getObjectArrayVal(jsFile, "accessors", accessors, _file.defined)) {
-            foreach(const QJsonValue & accVal, accessors) {
-                if (accVal.isObject()) {
-                    success = success && addAccessor(accVal.toObject());
-                }
-            }
-        }
-
-        QJsonArray animations;
-        if (getObjectArrayVal(jsFile, "animations", animations, _file.defined)) {
-            foreach(const QJsonValue & animVal, accessors) {
-                if (animVal.isObject()) {
-                    success = success && addAnimation(animVal.toObject());
-                }
-            }
-        }
-
-        QJsonArray bufferViews;
-        if (getObjectArrayVal(jsFile, "bufferViews", bufferViews, _file.defined)) {
-            foreach(const QJsonValue & bufviewVal, bufferViews) {
-                if (bufviewVal.isObject()) {
-                    success = success && addBufferView(bufviewVal.toObject());
-                }
-            }
-        }
-
-        QJsonArray buffers;
-        if (getObjectArrayVal(jsFile, "buffers", buffers, _file.defined)) {
-            foreach(const QJsonValue & bufVal, buffers) {
-                if (bufVal.isObject()) {
-                    success = success && addBuffer(bufVal.toObject());
-                }
-            }
-        }
-
-        QJsonArray cameras;
-        if (getObjectArrayVal(jsFile, "cameras", cameras, _file.defined)) {
-            foreach(const QJsonValue & camVal, cameras) {
-                if (camVal.isObject()) {
-                    success = success && addCamera(camVal.toObject());
-                }
-            }
-        }
-
-        QJsonArray images;
-        if (getObjectArrayVal(jsFile, "images", images, _file.defined)) {
-            foreach(const QJsonValue & imgVal, images) {
-                if (imgVal.isObject()) {
-                    success = success && addImage(imgVal.toObject());
-                }
-            }
-        }
-
-        QJsonArray materials;
-        if (getObjectArrayVal(jsFile, "materials", materials, _file.defined)) {
-            foreach(const QJsonValue & matVal, materials) {
-                if (matVal.isObject()) {
-                    success = success && addMaterial(matVal.toObject());
-                }
-            }
-        }
-
-        QJsonArray meshes;
-        if (getObjectArrayVal(jsFile, "meshes", meshes, _file.defined)) {
-            foreach(const QJsonValue & meshVal, meshes) {
-                if (meshVal.isObject()) {
-                    success = success && addMesh(meshVal.toObject());
-                }
-            }
-        }
-
-        QJsonArray nodes;
-        if (getObjectArrayVal(jsFile, "nodes", nodes, _file.defined)) {
-            foreach(const QJsonValue & nodeVal, nodes) {
-                if (nodeVal.isObject()) {
-                    success = success && addNode(nodeVal.toObject());
-                }
-            }
-        }
-
-        QJsonArray samplers;
-        if (getObjectArrayVal(jsFile, "samplers", samplers, _file.defined)) {
-            foreach(const QJsonValue & samVal, samplers) {
-                if (samVal.isObject()) {
-                    success = success && addSampler(samVal.toObject());
-                }
-            }
-        }
-
-        QJsonArray scenes;
-        if (getObjectArrayVal(jsFile, "scenes", scenes, _file.defined)) {
-            foreach(const QJsonValue & sceneVal, scenes) {
-                if (sceneVal.isObject()) {
-                    success = success && addScene(sceneVal.toObject());
-                }
-            }
-        }
-
-        QJsonArray skins;
-        if (getObjectArrayVal(jsFile, "skins", skins, _file.defined)) {
-            foreach(const QJsonValue & skinVal, skins) {
-                if (skinVal.isObject()) {
-                    success = success && addSkin(skinVal.toObject());
-                }
-            }
-        }
-
-        QJsonArray textures;
-        if (getObjectArrayVal(jsFile, "textures", textures, _file.defined)) {
-            foreach(const QJsonValue & texVal, textures) {
-                if (texVal.isObject()) {
-                    success = success && addTexture(texVal.toObject());
-                }
-            }
-        }
-    }
-    return success;
-}
-
-glm::mat4 GLTFSerializer::getModelTransform(const GLTFNode& node) {
+glm::mat4 GLTFSerializer::getModelTransform(const cgltf_node& node) {
     glm::mat4 tmat = glm::mat4(1.0);
 
-    if (node.defined["matrix"] && node.matrix.size() == 16) {
+    if (node.has_matrix) {
         tmat = glm::mat4(node.matrix[0], node.matrix[1], node.matrix[2], node.matrix[3],
             node.matrix[4], node.matrix[5], node.matrix[6], node.matrix[7],
             node.matrix[8], node.matrix[9], node.matrix[10], node.matrix[11],
             node.matrix[12], node.matrix[13], node.matrix[14], node.matrix[15]);
     } else {
 
-        if (node.defined["scale"] && node.scale.size() == 3) {
+        if (node.has_scale) {
             glm::vec3 scale = glm::vec3(node.scale[0], node.scale[1], node.scale[2]);
             glm::mat4 s = glm::mat4(1.0);
             s = glm::scale(s, scale);
             tmat = s * tmat;
         }
 
-        if (node.defined["rotation"] && node.rotation.size() == 4) {
+        if (node.has_rotation) {
             //quat(x,y,z,w) to quat(w,x,y,z)
             glm::quat rotquat = glm::quat(node.rotation[3], node.rotation[0], node.rotation[1], node.rotation[2]);
             tmat = glm::mat4_cast(rotquat) * tmat;
         }
 
-        if (node.defined["translation"] && node.translation.size() == 3) {
+        if (node.has_translation) {
             glm::vec3 trans = glm::vec3(node.translation[0], node.translation[1], node.translation[2]);
             glm::mat4 t = glm::mat4(1.0);
             t = glm::translate(t, trans);
@@ -811,85 +111,151 @@ glm::mat4 GLTFSerializer::getModelTransform(const GLTFNode& node) {
     return tmat;
 }
 
-void GLTFSerializer::getSkinInverseBindMatrices(std::vector<std::vector<float>>& inverseBindMatrixValues) {
-    for (auto &skin : _file.skins) {
-        GLTFAccessor& indicesAccessor = _file.accessors[skin.inverseBindMatrices];
+bool GLTFSerializer::getSkinInverseBindMatrices(std::vector<std::vector<float>>& inverseBindMatrixValues) {
+    for (size_t i = 0; i < _data->skins_count; i++) {
+        auto &skin = _data->skins[i];
+
+        if (skin.inverse_bind_matrices == NULL) {
+            return false;
+        }
+
+        cgltf_accessor &matricesAccessor = *skin.inverse_bind_matrices;
         QVector<float> matrices;
-        addArrayFromAccessor(indicesAccessor, matrices);
+        if (matricesAccessor.type != cgltf_type_mat4) {
+            return false;
+        }
+        matrices.resize((int)matricesAccessor.count * 16);
+        size_t numFloats = cgltf_accessor_unpack_floats(&matricesAccessor, matrices.data(), matricesAccessor.count * 16);
+        Q_ASSERT(numFloats == matricesAccessor.count * 16);
         inverseBindMatrixValues.push_back(std::vector<float>(matrices.begin(), matrices.end()));
     }
+    return true;
 }
 
-void GLTFSerializer::generateTargetData(int index, float weight, QVector<glm::vec3>& returnVector) {
-    GLTFAccessor& accessor = _file.accessors[index];
+bool GLTFSerializer::generateTargetData(cgltf_accessor *accessor, float weight, QVector<glm::vec3>& returnVector) {
     QVector<float> storedValues;
-    addArrayFromAccessor(accessor, storedValues);
+    if(accessor == nullptr) {
+        return false;
+    }
+    if (accessor->type != cgltf_type_vec3) {
+        return false;
+    }
+    storedValues.resize((int)accessor->count * 3);
+    size_t numFloats = cgltf_accessor_unpack_floats(accessor, storedValues.data(), accessor->count * 3);
+    if (numFloats != accessor->count * 3) {
+        return false;
+    }
+
     for (int n = 0; n + 2 < storedValues.size(); n = n + 3) {
         returnVector.push_back(glm::vec3(weight * storedValues[n], weight * storedValues[n + 1], weight * storedValues[n + 2]));
     }
+    return true;
+}
+
+bool findNodeInPointerArray(const cgltf_node *nodePointer, cgltf_node **nodes, size_t arraySize, size_t &index) {
+    for (size_t i = 0; i < arraySize; i++) {
+        if (nodes[i] == nodePointer) {
+            index = i;
+            return true;
+        }
+    }
+    return false;
+}
+
+template<typename T> bool findPointerInArray(const T *pointer, const T *array, size_t arraySize, size_t &index) {
+    for (size_t i = 0; i < arraySize; i++) {
+        if (&array[i] == pointer) {
+            index = i;
+            return true;
+        }
+    }
+    return false;
+}
+
+bool findAttribute(const QString &name, const cgltf_attribute *attributes, size_t numAttributes, size_t &index) {
+    std::string nameString = name.toStdString();
+    for (size_t i = 0; i < numAttributes; i++) {
+        if (attributes->name == nullptr) {
+            qDebug(modelformat) << "GLTFSerializer: attribute with a null pointer name string";
+        } else {
+            if (strcmp(nameString.c_str(), attributes->name) == 0) {
+                index = i;
+                return true;
+            }
+        }
+    }
+    return false;
 }
 
 bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& url) {
     hfmModel.originalURL = url.toString();
 
-    int numNodes = _file.nodes.size();
+    int numNodes = (int)_data->nodes_count;
 
     //Build dependencies
     QVector<int> parents;
     QVector<int> sortedNodes;
     parents.fill(-1, numNodes);
     sortedNodes.reserve(numNodes);
-    int nodecount = 0;
-    foreach(auto &node, _file.nodes) {
-        foreach(int child, node.children) {
-            parents[child] = nodecount;
+    for(int index = 0; index < numNodes; index++) {
+        auto &node = _data->nodes[index];
+        for(size_t childIndexInParent = 0; childIndexInParent < node.children_count; childIndexInParent++) {
+            cgltf_node *child = node.children[childIndexInParent];
+            size_t childIndex = 0;
+            if (!findPointerInArray(child, _data->nodes, _data->nodes_count, childIndex)) {
+                qDebug(modelformat) << "findPointerInArray failed for model: " << _url;
+                hfmModel.loadErrorCount++;
+                return false;
+            }
+            parents[(int)childIndex] = index;
         }
-        sortedNodes.push_back(nodecount);
-        ++nodecount;
+        sortedNodes.push_back(index);
     }
 
-
     // Build transforms
-    nodecount = 0;
-    foreach(auto &node, _file.nodes) {
+    typedef QVector<glm::mat4> NodeTransforms;
+    QVector<NodeTransforms> transforms;
+    transforms.resize(numNodes);
+    for (int index = 0; index < numNodes; index++) {
         // collect node transform
-        _file.nodes[nodecount].transforms.push_back(getModelTransform(node));
-        int parentIndex = parents[nodecount];
+        auto &node = _data->nodes[index];
+        transforms[index].push_back(getModelTransform(node));
+        int parentIndex = parents[index];
         while (parentIndex != -1) {
-            const auto& parentNode = _file.nodes[parentIndex];
+            const auto& parentNode = _data->nodes[parentIndex];
             // collect transforms for a node's parents, grandparents, etc.
-            _file.nodes[nodecount].transforms.push_back(getModelTransform(parentNode));
+            transforms[index].push_back(getModelTransform(parentNode));
             parentIndex = parents[parentIndex];
         }
-        ++nodecount;
     }
 
-
     // since parent indices must exist in the sorted list before any of their children, sortedNodes might not be initialized in the correct order
     // therefore we need to re-initialize the order in which nodes will be parsed
     QVector<bool> hasBeenSorted;
     hasBeenSorted.fill(false, numNodes);
-    int i = 0;  // initial index
-    while (i < numNodes) {
-        int currentNode = sortedNodes[i];
-        int parentIndex = parents[currentNode];
-        if (parentIndex == -1 || hasBeenSorted[parentIndex]) {
-            hasBeenSorted[currentNode] = true;
-            ++i;
-        } else {
-            int j = i + 1; // index of node to be sorted
-            while (j < numNodes) {
-                int nextNode = sortedNodes[j];
-                parentIndex = parents[nextNode];
-                if (parentIndex == -1 || hasBeenSorted[parentIndex]) {
-                    // swap with currentNode
-                    hasBeenSorted[nextNode] = true;
-                    sortedNodes[i] = nextNode;
-                    sortedNodes[j] = currentNode;
-                    ++i;
-                    currentNode = sortedNodes[i];
+    {
+        int i = 0;  // initial index
+        while (i < numNodes) {
+            int currentNode = sortedNodes[i];
+            int parentIndex = parents[currentNode];
+            if (parentIndex == -1 || hasBeenSorted[parentIndex]) {
+                hasBeenSorted[currentNode] = true;
+                ++i;
+            } else {
+                int j = i + 1;  // index of node to be sorted
+                while (j < numNodes) {
+                    int nextNode = sortedNodes[j];
+                    parentIndex = parents[nextNode];
+                    if (parentIndex == -1 || hasBeenSorted[parentIndex]) {
+                        // swap with currentNode
+                        hasBeenSorted[nextNode] = true;
+                        sortedNodes[i] = nextNode;
+                        sortedNodes[j] = currentNode;
+                        ++i;
+                        currentNode = sortedNodes[i];
+                    }
+                    ++j;
                 }
-                ++j;
             }
         }
     }
@@ -911,13 +277,13 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
     globalTransforms.resize(numNodes);
 
     for (int nodeIndex : sortedNodes) {
-        auto& node = _file.nodes[nodeIndex];
+        auto& node = _data->nodes[nodeIndex];
 
         joint.parentIndex = parents[nodeIndex];
         if (joint.parentIndex != -1) {
             joint.parentIndex = originalToNewNodeIndexMap[joint.parentIndex];
         }
-        joint.transform = node.transforms.first();
+        joint.transform = transforms[nodeIndex].first();
         joint.translation = extractTranslation(joint.transform);
         joint.rotation = glmExtractRotation(joint.transform);
         glm::vec3 scale = extractScale(joint.transform);
@@ -951,24 +317,34 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
     jointInverseBindTransforms.resize(numNodes);
     globalBindTransforms.resize(numNodes);
 
-    hfmModel.hasSkeletonJoints = !_file.skins.isEmpty();
+    hfmModel.hasSkeletonJoints = _data->skins_count > 0;
     if (hfmModel.hasSkeletonJoints) {
         std::vector<std::vector<float>> inverseBindValues;
-        getSkinInverseBindMatrices(inverseBindValues);
+        if (!getSkinInverseBindMatrices(inverseBindValues)) {
+            qDebug(modelformat) << "GLTFSerializer::getSkinInverseBindMatrices: wrong matrices accessor type for model: " << _url;
+            hfmModel.loadErrorCount++;
+            return false;
+        }
 
         for (int jointIndex = 0; jointIndex < numNodes; ++jointIndex) {
             int nodeIndex = sortedNodes[jointIndex];
             auto joint = hfmModel.joints[jointIndex];
 
-            for (int s = 0; s < _file.skins.size(); ++s) {
-                const auto& skin = _file.skins[s];
-                int matrixIndex = skin.joints.indexOf(nodeIndex);
-                joint.isSkeletonJoint = skin.joints.contains(nodeIndex);
+            for (size_t s = 0; s < _data->skins_count; ++s) {
+                const auto& skin = _data->skins[s];
+                size_t jointNodeIndex = 0;
+                joint.isSkeletonJoint = findNodeInPointerArray(&_data->nodes[nodeIndex], skin.joints, skin.joints_count, jointNodeIndex);
 
                 // build inverse bind matrices
                 if (joint.isSkeletonJoint) {
+                    size_t matrixIndex = jointNodeIndex;
                     std::vector<float>& value = inverseBindValues[s];
-                    int matrixCount = 16 * matrixIndex;
+                    size_t matrixCount = 16 * matrixIndex;
+                    if (matrixCount + 15 >= value.size()) {
+                        qDebug(modelformat) << "GLTFSerializer::buildGeometry: not enough entries in jointInverseBindTransforms: " << _url;
+                        hfmModel.loadErrorCount++;
+                        return false;
+                    }
                     jointInverseBindTransforms[jointIndex] =
                         glm::mat4(value[matrixCount], value[matrixCount + 1], value[matrixCount + 2], value[matrixCount + 3],
                             value[matrixCount + 4], value[matrixCount + 5], value[matrixCount + 6], value[matrixCount + 7],
@@ -992,15 +368,15 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
     // Build materials
     QVector<QString> materialIDs;
     QString unknown = "Default";
-    int ukcount = 0;
-    foreach(auto material, _file.materials) {
-        if (!material.defined["name"]) {
-            QString name = unknown + QString::number(++ukcount);
-            material.name = name;
-            material.defined.insert("name", true);
+    for (size_t i = 0; i < _data->materials_count; i++) {
+        auto &material = _data->materials[i];
+        QString mid;
+        if (material.name != nullptr) {
+            mid = QString(material.name);
+        }else{
+            mid = QString::number(i);
         }
 
-        QString mid = material.name;
         materialIDs.push_back(mid);
     }
 
@@ -1008,19 +384,18 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
         QString& matid = materialIDs[i];
         hfmModel.materials[matid] = HFMMaterial();
         HFMMaterial& hfmMaterial = hfmModel.materials[matid];
-        hfmMaterial._material = std::make_shared<graphics::Material>();
         hfmMaterial.name = hfmMaterial.materialID = matid;
-        setHFMMaterial(hfmMaterial, _file.materials[i]);
+        setHFMMaterial(hfmMaterial, _data->materials[i]);
     }
 
 
     // Build meshes
-    nodecount = 0;
+    int nodeCount = 0;
     hfmModel.meshExtents.reset();
     for (int nodeIndex : sortedNodes) {
-        auto& node = _file.nodes[nodeIndex];
+        auto& node = _data->nodes[nodeIndex];
 
-        if (node.defined["mesh"]) {
+        if (node.mesh != nullptr) {
 
             hfmModel.meshes.append(HFMMesh());
             HFMMesh& mesh = hfmModel.meshes[hfmModel.meshes.size() - 1];
@@ -1028,7 +403,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
 
             if (!hfmModel.hasSkeletonJoints) {
                 HFMCluster cluster;
-                cluster.jointIndex = nodecount;
+                cluster.jointIndex = nodeCount;
                 cluster.inverseBindMatrix = glm::mat4();
                 cluster.inverseBindTransform = Transform(cluster.inverseBindMatrix);
                 mesh.clusters.append(cluster);
@@ -1048,27 +423,27 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
             mesh.clusters.append(root);
 
             QList<QString> meshAttributes;
-            foreach(auto &primitive, _file.meshes[node.mesh].primitives) {
-                QList<QString> keys = primitive.attributes.values.keys();
-                foreach (auto &key, keys) {
+            for (size_t primitiveIndex = 0; primitiveIndex < node.mesh->primitives_count; primitiveIndex++) {
+                auto &primitive = node.mesh->primitives[primitiveIndex];
+                for (size_t attributeIndex = 0; attributeIndex < primitive.attributes_count; attributeIndex++) {
+                    auto &attribute = primitive.attributes[attributeIndex];
+                    QString key(attribute.name);
                     if (!meshAttributes.contains(key)) {
                         meshAttributes.push_back(key);
                     }
                 }
             }
 
-            foreach(auto &primitive, _file.meshes[node.mesh].primitives) {
+            for (size_t primitiveIndex = 0; primitiveIndex < node.mesh->primitives_count; primitiveIndex++) {
+                auto &primitive = node.mesh->primitives[primitiveIndex];
                 HFMMeshPart part = HFMMeshPart();
 
-                int indicesAccessorIdx = primitive.indices;
-
-                if (indicesAccessorIdx > _file.accessors.size()) {
-                    qWarning(modelformat) << "Indices accessor index is out of bounds for model " << _url;
+                if (primitive.indices == nullptr) {
+                    qDebug() << "No indices accessor for mesh: " << _url;
                     hfmModel.loadErrorCount++;
-                    continue;
+                    return false;
                 }
-
-                GLTFAccessor& indicesAccessor = _file.accessors[indicesAccessorIdx];
+                auto &indicesAccessor = primitive.indices;
 
                 // Buffers
                 QVector<int> indices;
@@ -1089,9 +464,10 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                 QVector<float> weights;
                 int weightStride = 4;
 
-                bool success = addArrayFromAccessor(indicesAccessor, indices);
+                indices.resize((int)indicesAccessor->count);
+                size_t readIndicesCount = cgltf_accessor_unpack_indices(indicesAccessor, indices.data(), sizeof(unsigned int), indicesAccessor->count);
 
-                if (!success) {
+                if (readIndicesCount != indicesAccessor->count) {
                     qWarning(modelformat) << "There was a problem reading glTF INDICES data for model " << _url;
                     hfmModel.loadErrorCount++;
                     continue;
@@ -1100,51 +476,58 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                 // Increment the triangle indices by the current mesh vertex count so each mesh part can all reference the same buffers within the mesh
                 int prevMeshVerticesCount = mesh.vertices.count();
 
-                QList<QString> keys = primitive.attributes.values.keys();
+                // For each vertex (stride is WEIGHTS_PER_VERTEX), it contains index of the cluster that given weight belongs to.
                 QVector<uint16_t> clusterJoints;
                 QVector<float> clusterWeights;
 
-                foreach(auto &key, keys) {
-                    int accessorIdx = primitive.attributes.values[key];
-
-                    if (accessorIdx > _file.accessors.size()) {
-                        qWarning(modelformat) << "Accessor index is out of bounds for model " << _url;
+                for (size_t attributeIndex = 0; attributeIndex < primitive.attributes_count; attributeIndex++) {
+                    if (primitive.attributes[attributeIndex].name == nullptr) {
+                        qDebug() << "Inalid accessor name for mesh: " << _url;
                         hfmModel.loadErrorCount++;
-                        continue;
+                        return false;
                     }
+                    QString key(primitive.attributes[attributeIndex].name);
 
-                    GLTFAccessor& accessor = _file.accessors[accessorIdx];
+                    if (primitive.attributes[attributeIndex].data == nullptr) {
+                        qDebug() << "Inalid accessor for mesh: " << _url;
+                        hfmModel.loadErrorCount++;
+                        return false;
+                    }
+                    auto accessor = primitive.attributes[attributeIndex].data;
+                    int accessorCount = (int)accessor->count;
 
                     if (key == "POSITION") {
-                        if (accessor.type != GLTFAccessorType::VEC3) {
+                        if (accessor->type != cgltf_type_vec3) {
                             qWarning(modelformat) << "Invalid accessor type on glTF POSITION data for model " << _url;
                             hfmModel.loadErrorCount++;
                             continue;
                         }
 
-                        success = addArrayFromAccessor(accessor, vertices);
-                        if (!success) {
+                        vertices.resize(accessorCount * 3);
+                        size_t floatCount = cgltf_accessor_unpack_floats(accessor, vertices.data(), accessor->count * 3);
+                        if (floatCount != accessor->count * 3) {
                             qWarning(modelformat) << "There was a problem reading glTF POSITION data for model " << _url;
                             hfmModel.loadErrorCount++;
                             continue;
                         }
                     } else if (key == "NORMAL") {
-                        if (accessor.type != GLTFAccessorType::VEC3) {
+                        if (accessor->type != cgltf_type_vec3) {
                             qWarning(modelformat) << "Invalid accessor type on glTF NORMAL data for model " << _url;
                             hfmModel.loadErrorCount++;
                             continue;
                         }
 
-                        success = addArrayFromAccessor(accessor, normals);
-                        if (!success) {
+                        normals.resize(accessorCount * 3);
+                        size_t floatCount = cgltf_accessor_unpack_floats(accessor, normals.data(), accessor->count * 3);
+                        if (floatCount != accessor->count * 3) {
                             qWarning(modelformat) << "There was a problem reading glTF NORMAL data for model " << _url;
                             hfmModel.loadErrorCount++;
                             continue;
                         }
                     } else if (key == "TANGENT") {
-                        if (accessor.type == GLTFAccessorType::VEC4) {
+                        if (accessor->type == cgltf_type_vec4) {
                             tangentStride = 4;
-                        } else if (accessor.type == GLTFAccessorType::VEC3) {
+                        } else if (accessor->type == cgltf_type_vec3) {
                             tangentStride = 3;
                         } else {
                             qWarning(modelformat) << "Invalid accessor type on glTF TANGENT data for model " << _url;
@@ -1152,43 +535,46 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                             continue;
                         }
 
-                        success = addArrayFromAccessor(accessor, tangents);
-                        if (!success) {
+                        tangents.resize(accessorCount * tangentStride);
+                        size_t floatCount = cgltf_accessor_unpack_floats(accessor, tangents.data(), accessor->count * tangentStride);
+                        if (floatCount != accessor->count * tangentStride) {
                             qWarning(modelformat) << "There was a problem reading glTF TANGENT data for model " << _url;
                             hfmModel.loadErrorCount++;
                             tangentStride = 0;
                             continue;
                         }
                     } else if (key == "TEXCOORD_0") {
-                        success = addArrayFromAccessor(accessor, texcoords);
-                        if (!success) {
-                            qWarning(modelformat) << "There was a problem reading glTF TEXCOORD_0 data for model " << _url;
-                            hfmModel.loadErrorCount++;
-                            continue;
-                        }
-
-                        if (accessor.type != GLTFAccessorType::VEC2) {
+                        if (accessor->type != cgltf_type_vec2) {
                             qWarning(modelformat) << "Invalid accessor type on glTF TEXCOORD_0 data for model " << _url;
                             hfmModel.loadErrorCount++;
                             continue;
                         }
-                    } else if (key == "TEXCOORD_1") {
-                        success = addArrayFromAccessor(accessor, texcoords2);
-                        if (!success) {
-                            qWarning(modelformat) << "There was a problem reading glTF TEXCOORD_1 data for model " << _url;
+
+                        texcoords.resize(accessorCount * 2);
+                        size_t floatCount = cgltf_accessor_unpack_floats(accessor, texcoords.data(), accessor->count * 2);
+                        if (floatCount != accessor->count * 2) {
+                            qWarning(modelformat) << "There was a problem reading glTF TEXCOORD_0 data for model " << _url;
                             hfmModel.loadErrorCount++;
                             continue;
                         }
-
-                        if (accessor.type != GLTFAccessorType::VEC2) {
+                    } else if (key == "TEXCOORD_1") {
+                        if (accessor->type != cgltf_type_vec2) {
                             qWarning(modelformat) << "Invalid accessor type on glTF TEXCOORD_1 data for model " << _url;
                             hfmModel.loadErrorCount++;
                             continue;
                         }
+
+                        texcoords2.resize(accessorCount * 2);
+                        size_t floatCount = cgltf_accessor_unpack_floats(accessor, texcoords2.data(), accessor->count * 2);
+                        if (floatCount != accessor->count * 2) {
+                            qWarning(modelformat) << "There was a problem reading glTF TEXCOORD_1 data for model " << _url;
+                            hfmModel.loadErrorCount++;
+                            continue;
+                        }
                     } else if (key == "COLOR_0") {
-                        if (accessor.type == GLTFAccessorType::VEC4) {
+                        if (accessor->type == cgltf_type_vec4) {
                             colorStride = 4;
-                        } else if (accessor.type == GLTFAccessorType::VEC3) {
+                        } else if (accessor->type == cgltf_type_vec3) {
                             colorStride = 3;
                         } else {
                             qWarning(modelformat) << "Invalid accessor type on glTF COLOR_0 data for model " << _url;
@@ -1196,20 +582,21 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                             continue;
                         }
 
-                        success = addArrayFromAccessor(accessor, colors);
-                        if (!success) {
+                        colors.resize(accessorCount * colorStride);
+                        size_t floatCount = cgltf_accessor_unpack_floats(accessor, colors.data(), accessor->count * colorStride);
+                        if (floatCount != accessor->count * colorStride) {
                             qWarning(modelformat) << "There was a problem reading glTF COLOR_0 data for model " << _url;
                             hfmModel.loadErrorCount++;
                             continue;
                         }
                     } else if (key == "JOINTS_0") {
-                        if (accessor.type == GLTFAccessorType::VEC4) {
+                        if (accessor->type == cgltf_type_vec4) {
                             jointStride = 4;
-                        } else if (accessor.type == GLTFAccessorType::VEC3) {
+                        } else if (accessor->type == cgltf_type_vec3) {
                             jointStride = 3;
-                        } else if (accessor.type == GLTFAccessorType::VEC2) {
+                        } else if (accessor->type == cgltf_type_vec2) {
                             jointStride = 2;
-                        } else if (accessor.type == GLTFAccessorType::SCALAR) {
+                        } else if (accessor->type == cgltf_type_scalar) {
                             jointStride = 1;
                         } else {
                             qWarning(modelformat) << "Invalid accessor type on glTF JOINTS_0 data for model " << _url;
@@ -1217,20 +604,23 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                             continue;
                         }
 
-                        success = addArrayFromAccessor(accessor, joints);
-                        if (!success) {
-                            qWarning(modelformat) << "There was a problem reading glTF JOINTS_0 data for model " << _url;
-                            hfmModel.loadErrorCount++;
-                            continue;
+                        joints.resize(accessorCount * jointStride);
+                        cgltf_uint jointIndices[4];
+                        for (size_t i = 0; i < accessor->count; i++) {
+                            cgltf_accessor_read_uint(accessor, i, jointIndices, jointStride);
+                            for (int component = 0; component < jointStride; component++) {
+                                joints[(int)i * jointStride + component] = (uint16_t)jointIndices[component];
+                            }
                         }
+
                     } else if (key == "WEIGHTS_0") {
-                        if (accessor.type == GLTFAccessorType::VEC4) {
+                        if (accessor->type == cgltf_type_vec4) {
                             weightStride = 4;
-                        } else if (accessor.type == GLTFAccessorType::VEC3) {
+                        } else if (accessor->type == cgltf_type_vec3) {
                             weightStride = 3;
-                        } else if (accessor.type == GLTFAccessorType::VEC2) {
+                        } else if (accessor->type == cgltf_type_vec2) {
                             weightStride = 2;
-                        } else if (accessor.type == GLTFAccessorType::SCALAR) {
+                        } else if (accessor->type == cgltf_type_scalar) {
                             weightStride = 1;
                         } else {
                             qWarning(modelformat) << "Invalid accessor type on glTF WEIGHTS_0 data for model " << _url;
@@ -1238,8 +628,9 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                             continue;
                         }
 
-                        success = addArrayFromAccessor(accessor, weights);
-                        if (!success) {
+                        weights.resize(accessorCount * weightStride);
+                        size_t floatCount = cgltf_accessor_unpack_floats(accessor, weights.data(), accessor->count * weightStride);
+                        if (floatCount != accessor->count * weightStride) {
                             qWarning(modelformat) << "There was a problem reading glTF WEIGHTS_0 data for model " << _url;
                             hfmModel.loadErrorCount++;
                             continue;
@@ -1280,7 +671,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                         if (v1_index + 2 >= vertices.size() || v2_index + 2 >= vertices.size() || v3_index + 2 >= vertices.size()) {
                             qWarning(modelformat) << "Indices out of range for model " << _url;
                             hfmModel.loadErrorCount++;
-                            break;
+                            return false;
                         }
 
                         glm::vec3 v1 = glm::vec3(vertices[v1_index], vertices[v1_index + 1], vertices[v1_index + 2]);
@@ -1534,22 +925,21 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                             continue;
                         }
 
-                        if ( _file.skins.length() <= node.skin ) {
-                            qCWarning(modelformat) << "Trying to read past end of _file.skins at" << node.skin;
-                            hfmModel.loadErrorCount++;
-                            continue;
-                        }
-
-                        if ( _file.skins[node.skin].joints.length() <= clusterJoints[c]) {
+                        if ( node.skin->joints_count <= clusterJoints[c]) {
                             qCWarning(modelformat) << "Trying to read past end of _file.skins[node.skin].joints at" << clusterJoints[c]
-                                                   << "; there are only" << _file.skins[node.skin].joints.length() << "for skin" << node.skin;
+                                                   << "; there are only" << node.skin->joints_count << "for skin" << node.skin->name;
                             hfmModel.loadErrorCount++;
                             continue;
                         }
 
-
+                        size_t jointIndex = 0;
+                        if (!findPointerInArray(node.skin->joints[clusterJoints[c]], _data->nodes, _data->nodes_count, jointIndex)) {
+                            qCWarning(modelformat) << "Cannot find the joint " << node.skin->joints[clusterJoints[c]]->name <<" in joint array";
+                            hfmModel.loadErrorCount++;
+                            continue;
+                        }
                         mesh.clusterIndices[prevMeshClusterIndexCount + c] =
-                            originalToNewNodeIndexMap[_file.skins[node.skin].joints[clusterJoints[c]]];
+                            originalToNewNodeIndexMap[(int)jointIndex];
                     }
 
                     // normalize and compress to 16-bits
@@ -1568,26 +958,31 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                         } else {
                             mesh.clusterWeights[prevMeshClusterWeightCount + j] = (uint16_t)((float)(UINT16_MAX) + ALMOST_HALF);
                         }
-                        for (int clusterIndex = 0; clusterIndex < mesh.clusters.size() - 1; ++clusterIndex) {
+                        for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) {
+                            int clusterIndex = mesh.clusterIndices[prevMeshClusterIndexCount + k];
                             ShapeVertices& points = hfmModel.shapeVertices.at(clusterIndex);
                             glm::vec3 globalMeshScale = extractScale(globalTransforms[nodeIndex]);
                             const glm::mat4 meshToJoint = glm::scale(glm::mat4(), globalMeshScale) * jointInverseBindTransforms[clusterIndex];
 
-                            // TODO: The entire clustering is probably broken and detailed collision shapes fail to generate due to it.
                             const uint16_t EXPANSION_WEIGHT_THRESHOLD = UINT16_MAX/4; // Equivalent of 0.25f?
-                            if (mesh.clusterWeights[j] >= EXPANSION_WEIGHT_THRESHOLD) {
-                                // TODO: fix transformed vertices being pushed back
-                                auto& vertex = mesh.vertices[i];
-                                const glm::mat4 vertexTransform = meshToJoint * (glm::translate(glm::mat4(), vertex));
-                                glm::vec3 transformedVertex = hfmModel.joints[clusterIndex].translation * (extractTranslation(vertexTransform));
+                            if (mesh.clusterWeights[prevMeshClusterWeightCount + k] >= EXPANSION_WEIGHT_THRESHOLD) {
+                                auto& vertex = mesh.vertices[prevMeshVerticesCount + i];
+                                const glm::mat4 vertexTransform = meshToJoint * glm::translate(vertex);
+                                glm::vec3 transformedVertex = extractTranslation(vertexTransform);
                                 points.push_back(transformedVertex);
                             }
                         }
                     }
                 }
 
-                if (primitive.defined["material"]) {
-                    part.materialID = materialIDs[primitive.material];
+                size_t materialIndex = 0;
+                if (primitive.material != nullptr && !findPointerInArray(primitive.material, _data->materials, _data->materials_count, materialIndex)) {
+                    qCWarning(modelformat) << "GLTFSerializer::buildGeometry: Invalid material pointer";
+                    hfmModel.loadErrorCount++;
+                    return false;
+                }
+                if (primitive.material != nullptr) {
+                    part.materialID = materialIDs[(int)materialIndex];
                 }
                 mesh.parts.push_back(part);
 
@@ -1599,7 +994,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                 }
 
                 // Build morph targets (blend shapes)
-                if (!primitive.targets.isEmpty()) {
+                if (primitive.targets_count) {
 
                     // Build list of blendshapes from FST and model.
                     typedef QPair<int, float> WeightedIndex;
@@ -1613,21 +1008,30 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                         auto mappings = blendshapeMappings.values(blendshapeName);
                         if (mappings.count() > 0) {
                             // Use blendshape from mapping.
-                            foreach(const QVariant& mapping, mappings) {
-                                auto blendshapeMapping = mapping.toList();
+                            foreach(const QVariant& mappingVariant, mappings) {
+                                auto blendshapeMapping = mappingVariant.toList();
                                 blendshapeIndices.insert(blendshapeMapping.at(0).toString(),
                                     WeightedIndex(i, blendshapeMapping.at(1).toFloat()));
                             }
                         } else {
                             // Use blendshape from model.
-                            if (_file.meshes[node.mesh].extras.targetNames.contains(blendshapeName)) {
-                                blendshapeIndices.insert(blendshapeName, WeightedIndex(i, 1.0f));
+                            std::string blendshapeNameString = blendshapeName.toStdString();
+                            for (size_t j = 0; j < node.mesh->target_names_count; j++) {
+                                if (strcmp(node.mesh->target_names[j], blendshapeNameString.c_str()) == 0) {
+                                    blendshapeIndices.insert(blendshapeName, WeightedIndex(i, 1.0f));
+                                    break;
+                                }
                             }
                         }
                     }
 
                     // If an FST isn't being used and the model is likely from ReadyPlayerMe, add blendshape synonyms.
-                    auto fileTargetNames = _file.meshes[node.mesh].extras.targetNames;
+                    QVector<QString> fileTargetNames;
+                    fileTargetNames.reserve((int)node.mesh->target_names_count);
+                    for (size_t i = 0; i < node.mesh->target_names_count; i++) {
+                        fileTargetNames.push_back(QString(node.mesh->target_names[i]));
+                    }
+
                     bool likelyReadyPlayerMeFile =
                            fileTargetNames.contains("browOuterUpLeft")
                         && fileTargetNames.contains("browInnerUp")
@@ -1658,7 +1062,11 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                     }
                     auto keys = blendshapeIndices.keys();
                     auto values = blendshapeIndices.values();
-                    auto names = _file.meshes[node.mesh].extras.targetNames;
+                    QVector<QString> names;
+                    names.reserve((int)node.mesh->target_names_count);
+                    for (size_t i = 0; i < node.mesh->target_names_count; i++) {
+                        names.push_back(QString(node.mesh->target_names[i]));
+                    }
 
                     for (int weightedIndex = 0; weightedIndex < keys.size(); ++weightedIndex) {
                         float weight = 1.0f;
@@ -1682,11 +1090,21 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                         QVector<glm::vec3> normals;
                         QVector<glm::vec3> vertices;
 
-                        if (target.values.contains((QString) "NORMAL")) {
-                            generateTargetData(target.values.value((QString) "NORMAL"), weight, normals);
+                        size_t normalAttributeIndex = 0;
+                        if (findAttribute("NORMAL", target.attributes, target.attributes_count, normalAttributeIndex)) {
+                            if (!generateTargetData(target.attributes[normalAttributeIndex].data, weight, normals)) {
+                                qWarning(modelformat) << "Invalid NORMAL accessor on generateTargetData vertices for model " << _url;
+                                hfmModel.loadErrorCount++;
+                                return false;
+                            }
                         }
-                        if (target.values.contains((QString) "POSITION")) {
-                            generateTargetData(target.values.value((QString) "POSITION"), weight, vertices);
+                        size_t positionAttributeIndex = 0;
+                        if (findAttribute("POSITION", target.attributes, target.attributes_count, positionAttributeIndex)) {
+                            if (!generateTargetData(target.attributes[positionAttributeIndex].data, weight, vertices)) {
+                                qWarning(modelformat) << "Invalid POSITION accessor on generateTargetData vertices for model " << _url;
+                                hfmModel.loadErrorCount++;
+                                return false;
+                            }
                         }
 
                         if (blendshape.indices.size() < prevMeshVerticesCount + vertices.size()) {
@@ -1694,12 +1112,23 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
                             blendshape.vertices.resize(prevMeshVerticesCount + vertices.size());
                             blendshape.normals.resize(prevMeshVerticesCount + vertices.size());
                         }
+                        //TODO: it looks like this can support sparse encoding, since there are indices?
                         for (int i = 0; i < vertices.size(); i++) {
                             blendshape.indices[prevMeshVerticesCount + i] = prevMeshVerticesCount + i;
                             blendshape.vertices[prevMeshVerticesCount + i] += vertices.value(i);
-                            blendshape.normals[prevMeshVerticesCount + i] += normals.value(i);
+                            // Prevent out-of-bounds access if blendshape normals are not available
+                            if (i < normals.size()) {
+                                blendshape.normals[prevMeshVerticesCount + i] += normals.value(i);
+                            } else {
+                                if (prevMeshVerticesCount + i < mesh.normals.size()) {
+                                    blendshape.normals[prevMeshVerticesCount + i] = mesh.normals[prevMeshVerticesCount + i];
+                                } else {
+                                    qWarning(modelformat) << "Blendshape has more vertices than original mesh " << _url;
+                                    hfmModel.loadErrorCount++;
+                                    return false;
+                                }
+                            }
                         }
-
                     }
                 }
 
@@ -1720,7 +1149,7 @@ bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::VariantHash&
 
             mesh.meshIndex = hfmModel.meshes.size();
         }
-        ++nodecount;
+        ++nodeCount;
     }
 
     return true;
@@ -1752,45 +1181,58 @@ HFMModel::Pointer GLTFSerializer::read(const hifi::ByteArray& data, const hifi::
         _url = hifi::URL(QFileInfo(localFileName).absoluteFilePath());
     }
 
-    if (parseGLTF(data)) {
-        //_file.dump();
-        auto hfmModelPtr = std::make_shared<HFMModel>();
-        HFMModel& hfmModel = *hfmModelPtr;
-        buildGeometry(hfmModel, mapping, _url);
-
-        //hfmModel.debugDump();
-        //glTFDebugDump();
-
-        return hfmModelPtr;
-    } else {
+    cgltf_options options = {};
+    cgltf_result result = cgltf_parse(&options, data.data(), data.size(), &_data);
+    if (result != cgltf_result_success) {
         qCDebug(modelformat) << "Error parsing GLTF file.";
+        return nullptr;
+    }
+    cgltf_load_buffers(&options, _data, NULL);
+    for (size_t i = 0; i < _data->buffers_count; i++) {
+        cgltf_buffer &buffer = _data->buffers[i];
+        if (buffer.data == nullptr) {
+            if (!readBinary(buffer.uri, buffer)) {
+                qCDebug(modelformat) << "Error parsing GLTF file.";
+                return nullptr;
+            }
+        }
     }
 
-    return nullptr;
+
+    auto hfmModelPtr = std::make_shared<HFMModel>();
+    HFMModel& hfmModel = *hfmModelPtr;
+    buildGeometry(hfmModel, mapping, _url);
+
+    return hfmModelPtr;
 }
 
-bool GLTFSerializer::readBinary(const QString& url, hifi::ByteArray& outdata) {
+bool GLTFSerializer::readBinary(const QString& url, cgltf_buffer &buffer) {
     bool success;
+    hifi::ByteArray outdata;
 
+    // Is this part already done by cgltf?
     if (url.contains("data:application/octet-stream;base64,")) {
+        qDebug() << "GLTFSerializer::readBinary: base64";
         outdata = requestEmbeddedData(url);
         success = !outdata.isEmpty();
     } else {
         hifi::URL binaryUrl = _url.resolved(url);
         std::tie<bool, hifi::ByteArray>(success, outdata) = requestData(binaryUrl);
     }
+    if (success) {
+        if(buffer.size == (size_t)outdata.size()) {
+            _externalData.push_back(outdata);
+            buffer.data = _externalData.last().data();
+            buffer.data_free_method = cgltf_data_free_method_none;
+        } else {
+            qDebug() << "Buffer size mismatch for model: " << _url;
+            success = false;
+        }
+    }
 
     return success;
 }
 
-bool GLTFSerializer::doesResourceExist(const QString& url) {
-    if (_url.isEmpty()) {
-        return false;
-    }
-    hifi::URL candidateUrl = _url.resolved(url);
-    return DependencyManager::get<ResourceManager>()->resourceExists(candidateUrl);
-}
-
 std::tuple<bool, hifi::ByteArray> GLTFSerializer::requestData(hifi::URL& url) {
     auto request = DependencyManager::get<ResourceManager>()->createResourceRequest(
         nullptr, url, true, -1, "GLTFSerializer::requestData");
@@ -1841,265 +1283,246 @@ QNetworkReply* GLTFSerializer::request(hifi::URL& url, bool isTest) {
     return netReply;                // trying to sync later on.
 }
 
-HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) {
-    HFMTexture fbxtex = HFMTexture();
-    fbxtex.texcoordSet = 0;
+HFMTexture GLTFSerializer::getHFMTexture(const cgltf_texture *texture) {
+    HFMTexture hfmTex = HFMTexture();
+    hfmTex.texcoordSet = 0;
 
-    if (texture.defined["source"]) {
-        QString url = _file.images[texture.source].uri;
+    auto image = texture->image;
 
-        QString fname = hifi::URL(url).fileName();
-        hifi::URL textureUrl = _url.resolved(url);
-        fbxtex.name = fname;
-        fbxtex.filename = textureUrl.toEncoded();
-
-        if (_url.path().endsWith("glb") && !_glbBinary.isEmpty()) {
-            int bufferView = _file.images[texture.source].bufferView;
-
-            GLTFBufferView& imagesBufferview = _file.bufferviews[bufferView];
-            int offset = imagesBufferview.byteOffset;
-            int length = imagesBufferview.byteLength;
-
-            fbxtex.content = _glbBinary.mid(offset, length);
-            fbxtex.filename = textureUrl.toEncoded().append(texture.source);
-        }
-
-        if (url.contains("data:image/jpeg;base64,") || url.contains("data:image/png;base64,")) {
-            fbxtex.content = requestEmbeddedData(url);
+    // Check for WebP extension
+    for (size_t i = 0; i < texture->extensions_count; i++) {
+        auto &extension = texture->extensions[i];
+        if (extension.name != nullptr
+            && strcmp(extension.name, "EXT_texture_webp") == 0
+            && extension.data != nullptr) {
+            QJsonDocument webPExtension = QJsonDocument::fromJson(extension.data);
+            if (!webPExtension.isNull() && webPExtension["source"].isDouble()) {
+                int imageIndex = webPExtension["source"].isDouble();
+                if (imageIndex > 0 && (size_t)imageIndex < _data->images_count) {
+                    image = &_data->images[(int)(webPExtension["source"].toDouble())];
+                    break;
+                }
+            }
         }
     }
-    return fbxtex;
+
+    if (image) {
+        QString url = image->uri;
+
+        QString fileName = hifi::URL(url).fileName();
+        hifi::URL textureUrl = _url.resolved(url);
+        hfmTex.name = fileName;
+        hfmTex.filename = textureUrl.toEncoded();
+
+        if (_url.path().endsWith("glb")) {
+            cgltf_buffer_view *bufferView = image->buffer_view;
+
+            size_t offset = bufferView->offset;
+            int length = (int)bufferView->size;
+
+            size_t imageIndex = 0;
+            if (!findPointerInArray(image, _data->images, _data->images_count, imageIndex)) {
+                // This should never happen. It would mean a bug in cgltf library.
+                qDebug(modelformat) << "GLTFSerializer::getHFMTexture: can't find texture in the array";
+                return hfmTex;
+            }
+
+            if (offset + length > bufferView->buffer->size) {
+                qDebug(modelformat) << "GLTFSerializer::getHFMTexture: texture data to short";
+                return hfmTex;
+            }
+            hfmTex.content = QByteArray(static_cast<const char *>(bufferView->buffer->data) + offset, length);
+            hfmTex.filename = textureUrl.toEncoded().append(QString::number(imageIndex).toUtf8());
+        }
+
+        if (url.contains("data:image/jpeg;base64,") || url.contains("data:image/png;base64,") || url.contains("data:image/webp;base64,")) {
+            hfmTex.content = requestEmbeddedData(url);
+        }
+    }
+    return hfmTex;
 }
 
-void GLTFSerializer::setHFMMaterial(HFMMaterial& hfmMat, const GLTFMaterial& material) {
-    if (material.defined["alphaMode"]) {
-        hfmMat._material->setOpacityMapMode(material.alphaMode);
+void GLTFSerializer::setHFMMaterial(HFMMaterial& hfmMat, const cgltf_material& material) {
+    hfmMat._material = std::make_shared<graphics::Material>();
+    for (size_t i = 0; i < material.extensions_count; i++) {
+        auto& extension = material.extensions[i];
+        if (extension.name != nullptr) {
+            if (strcmp(extension.name, "VRMC_materials_mtoon") == 0 && extension.data != nullptr) {
+                hfmMat.isMToonMaterial = true;
+                auto mToonMaterial = std::make_shared<NetworkMToonMaterial>();
+                QJsonDocument mToonExtension = QJsonDocument::fromJson(extension.data);
+                if (!mToonExtension.isNull()) {
+                    if (mToonExtension["shadeColorFactor"].isArray()) {
+                        auto array = mToonExtension["shadeColorFactor"].toArray();
+                        glm::vec3 shadeLinear = glm::vec3(array[0].toDouble(), array[1].toDouble(), array[2].toDouble());
+                        glm::vec3 shade = ColorUtils::tosRGBVec3(shadeLinear);
+                        mToonMaterial->setShade(shade);
+                    }
+                    if (mToonExtension["shadeMultiplyTexture"].isObject()) {
+                        QJsonObject object = mToonExtension["shadeMultiplyTexture"].toObject();
+                        if (object["index"].isDouble() && object["index"].toInt() < (int)_data->textures_count) {
+                            hfmMat.shadeTexture = getHFMTexture(&_data->textures[object["index"].toInt()]);
+                        }
+                    }
+                    if (mToonExtension["shadingShiftFactor"].isDouble()) {
+                        mToonMaterial->setShadingShift(mToonExtension["shadingShiftFactor"].toDouble());
+                    }
+                    if (mToonExtension["shadingShiftTexture"].isObject()) {
+                        QJsonObject object = mToonExtension["shadingShiftTexture"].toObject();
+                        if (object["index"].isDouble() && object["index"].toInt() < (int)_data->textures_count) {
+                            hfmMat.shadingShiftTexture = getHFMTexture(&_data->textures[object["index"].toInt()]);
+                        }
+                    }
+                    if (mToonExtension["shadingToonyFactor"].isDouble()) {
+                        mToonMaterial->setShadingToony(mToonExtension["shadingToonyFactor"].toDouble());
+                    }
+                    if (mToonExtension["matcapFactor"].isArray()) {
+                        auto array = mToonExtension["matcapFactor"].toArray();
+                        glm::vec3 matcapLinear = glm::vec3(array[0].toDouble(), array[1].toDouble(), array[2].toDouble());
+                        glm::vec3 matcap = ColorUtils::tosRGBVec3(matcapLinear);
+                        mToonMaterial->setMatcap(matcap);
+                    }
+                    if (mToonExtension["matcapTexture"].isObject()) {
+                        QJsonObject object = mToonExtension["matcapTexture"].toObject();
+                        if (object["index"].isDouble() && object["index"].toInt() < (int)_data->textures_count) {
+                            hfmMat.matcapTexture = getHFMTexture(&_data->textures[object["index"].toInt()]);
+                        }
+                    }
+                    if (mToonExtension["parametricRimColorFactor"].isArray()) {
+                        auto array = mToonExtension["parametricRimColorFactor"].toArray();
+                        glm::vec3 parametricRimLinear = glm::vec3(array[0].toDouble(), array[1].toDouble(), array[2].toDouble());
+                        glm::vec3 parametricRim = ColorUtils::tosRGBVec3(parametricRimLinear);
+                        mToonMaterial->setParametricRim(parametricRim);
+                    }
+                    if (mToonExtension["parametricRimFresnelPowerFactor"].isDouble()) {
+                        mToonMaterial->setParametricRimFresnelPower(mToonExtension["parametricRimFresnelPowerFactor"].toDouble());
+                    }
+                    if (mToonExtension["parametricRimLiftFactor"].isDouble()) {
+                        mToonMaterial->setParametricRimLift(mToonExtension["parametricRimLiftFactor"].toDouble());
+                    }
+                    if (mToonExtension["rimMultiplyTexture"].isObject()) {
+                        QJsonObject object = mToonExtension["rimMultiplyTexture"].toObject();
+                        if (object["index"].isDouble() && object["index"].toInt() < (int)_data->textures_count) {
+                            hfmMat.rimTexture = getHFMTexture(&_data->textures[object["index"].toInt()]);
+                        }
+                    }
+                    if (mToonExtension["rimLightingMixFactor"].isDouble()) {
+                        mToonMaterial->setRimLightingMix(mToonExtension["rimLightingMixFactor"].toDouble());
+                    }
+                    // FIXME: Outlines are currently disabled because they're buggy
+                    //if (mToonExtension["outlineWidthMode"].isString()) {
+                    //    QString outlineWidthMode = mToonExtension["outlineWidthMode"].toString();
+                    //    if (outlineWidthMode == "none") {
+                    //        mToonMaterial->setOutlineWidthMode(NetworkMToonMaterial::OutlineWidthMode::OUTLINE_NONE);
+                    //    } else if (outlineWidthMode == "worldCoordinates") {
+                    //        mToonMaterial->setOutlineWidthMode(NetworkMToonMaterial::OutlineWidthMode::OUTLINE_WORLD);
+                    //    } else if (outlineWidthMode == "screenCoordinates") {
+                    //        mToonMaterial->setOutlineWidthMode(NetworkMToonMaterial::OutlineWidthMode::OUTLINE_SCREEN);
+                    //    }
+                    //}
+                    if (mToonExtension["outlineWidthFactor"].isDouble()) {
+                        mToonMaterial->setOutlineWidth(mToonExtension["outlineWidthFactor"].toDouble());
+                    }
+                    if (mToonExtension["outlineColorFactor"].isArray()) {
+                        auto array = mToonExtension["outlineColorFactor"].toArray();
+                        glm::vec3 outlineLinear = glm::vec3(array[0].toDouble(), array[1].toDouble(), array[2].toDouble());
+                        glm::vec3 outline = ColorUtils::tosRGBVec3(outlineLinear);
+                        mToonMaterial->setOutline(outline);
+                    }
+                    if (mToonExtension["uvAnimationMaskTexture"].isObject()) {
+                        QJsonObject object = mToonExtension["uvAnimationMaskTexture"].toObject();
+                        if (object["index"].isDouble() && object["index"].toInt() < (int)_data->textures_count) {
+                            hfmMat.uvAnimationTexture = getHFMTexture(&_data->textures[object["index"].toInt()]);
+                        }
+                    }
+                    if (mToonExtension["uvAnimationScrollXSpeedFactor"].isDouble()) {
+                        mToonMaterial->setUVAnimationScrollXSpeed(mToonExtension["uvAnimationScrollXSpeedFactor"].toDouble());
+                    }
+                    if (mToonExtension["uvAnimationScrollYSpeedFactor"].isDouble()) {
+                        mToonMaterial->setUVAnimationScrollYSpeed(mToonExtension["uvAnimationScrollYSpeedFactor"].toDouble());
+                    }
+                    if (mToonExtension["uvAnimationRotationSpeedFactor"].isDouble()) {
+                        mToonMaterial->setUVAnimationRotationSpeed(mToonExtension["uvAnimationRotationSpeedFactor"].toDouble());
+                    }
+                }
+                hfmMat._material = mToonMaterial;
+            }
+        }
+    }
+
+    if (material.alpha_mode == cgltf_alpha_mode_opaque) {
+        hfmMat._material->setOpacityMapMode(graphics::MaterialKey::OPACITY_MAP_OPAQUE);
+    } else if (material.alpha_mode == cgltf_alpha_mode_mask) {
+        hfmMat._material->setOpacityMapMode(graphics::MaterialKey::OPACITY_MAP_MASK);
+    } else if (material.alpha_mode == cgltf_alpha_mode_blend) {
+        hfmMat._material->setOpacityMapMode(graphics::MaterialKey::OPACITY_MAP_BLEND);
     } else {
         hfmMat._material->setOpacityMapMode(graphics::MaterialKey::OPACITY_MAP_OPAQUE); // GLTF defaults to opaque
     }
 
-    if (material.defined["alphaCutoff"]) {
-        hfmMat._material->setOpacityCutoff(material.alphaCutoff);
+    hfmMat._material->setOpacityCutoff(material.alpha_cutoff);
+
+    // VRMC_materials_mtoon takes precedence over KHR_materials_unlit
+    if (!hfmMat.isMToonMaterial) {
+        hfmMat._material->setUnlit(material.unlit);
     }
 
-    if (material.defined["doubleSided"] && material.doubleSided) {
+    if (material.double_sided) {
         hfmMat._material->setCullFaceMode(graphics::MaterialKey::CullFaceMode::CULL_NONE);
     }
 
-    if (material.defined["emissiveFactor"] && material.emissiveFactor.size() == 3) {
-        glm::vec3 emissiveLinear = glm::vec3(material.emissiveFactor[0], material.emissiveFactor[1], material.emissiveFactor[2]);
-        glm::vec3 emissive = ColorUtils::tosRGBVec3(emissiveLinear);
-        hfmMat._material->setEmissive(emissive);
-    }
+    glm::vec3 emissiveLinear = glm::vec3(material.emissive_factor[0], material.emissive_factor[1], material.emissive_factor[2]);
+    glm::vec3 emissive = ColorUtils::tosRGBVec3(emissiveLinear);
+    hfmMat._material->setEmissive(emissive);
 
-    if (material.defined["emissiveTexture"]) {
-        hfmMat.emissiveTexture = getHFMTexture(_file.textures[material.emissiveTexture]);
+    if (material.emissive_texture.texture != nullptr) {
+        hfmMat.emissiveTexture = getHFMTexture(material.emissive_texture.texture);
         hfmMat.useEmissiveMap = true;
     }
 
-    if (material.defined["normalTexture"]) {
-        hfmMat.normalTexture = getHFMTexture(_file.textures[material.normalTexture]);
+    if (material.normal_texture.texture != nullptr) {
+        hfmMat.normalTexture = getHFMTexture(material.normal_texture.texture);
         hfmMat.useNormalMap = true;
     }
 
-    if (material.defined["occlusionTexture"]) {
-        hfmMat.occlusionTexture = getHFMTexture(_file.textures[material.occlusionTexture]);
+    if (material.occlusion_texture.texture != nullptr) {
+        hfmMat.occlusionTexture = getHFMTexture(material.occlusion_texture.texture);
         hfmMat.useOcclusionMap = true;
     }
 
-    if (material.defined["pbrMetallicRoughness"]) {
+    if (material.has_pbr_metallic_roughness) {
         hfmMat.isPBSMaterial = true;
 
-        if (material.pbrMetallicRoughness.defined["metallicFactor"]) {
-            hfmMat.metallic = material.pbrMetallicRoughness.metallicFactor;
-            hfmMat._material->setMetallic(hfmMat.metallic);
-        }
-        if (material.pbrMetallicRoughness.defined["baseColorTexture"]) {
-            hfmMat.opacityTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]);
-            hfmMat.albedoTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.baseColorTexture]);
+        hfmMat.metallic = material.pbr_metallic_roughness.metallic_factor;
+        hfmMat._material->setMetallic(hfmMat.metallic);
+
+        if (material.pbr_metallic_roughness.base_color_texture.texture != nullptr) {
+            hfmMat.opacityTexture = getHFMTexture(material.pbr_metallic_roughness.base_color_texture.texture);
+            hfmMat.albedoTexture = getHFMTexture(material.pbr_metallic_roughness.base_color_texture.texture);
             hfmMat.useAlbedoMap = true;
         }
-        if (material.pbrMetallicRoughness.defined["metallicRoughnessTexture"]) {
-            hfmMat.roughnessTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]);
+        if (material.pbr_metallic_roughness.metallic_roughness_texture.texture) {
+            hfmMat.roughnessTexture = getHFMTexture(material.pbr_metallic_roughness.metallic_roughness_texture.texture);
             hfmMat.roughnessTexture.sourceChannel = image::ColorChannel::GREEN;
             hfmMat.useRoughnessMap = true;
-            hfmMat.metallicTexture = getHFMTexture(_file.textures[material.pbrMetallicRoughness.metallicRoughnessTexture]);
+            hfmMat.metallicTexture = getHFMTexture(material.pbr_metallic_roughness.metallic_roughness_texture.texture);
             hfmMat.metallicTexture.sourceChannel = image::ColorChannel::BLUE;
             hfmMat.useMetallicMap = true;
         }
-        if (material.pbrMetallicRoughness.defined["roughnessFactor"]) {
-            hfmMat._material->setRoughness(material.pbrMetallicRoughness.roughnessFactor);
-        }
-        if (material.pbrMetallicRoughness.defined["baseColorFactor"] &&
-            material.pbrMetallicRoughness.baseColorFactor.size() == 4) {
-            glm::vec3 lcolor = glm::vec3(material.pbrMetallicRoughness.baseColorFactor[0], material.pbrMetallicRoughness.baseColorFactor[1],
-                                         material.pbrMetallicRoughness.baseColorFactor[2]);
-            glm::vec3 dcolor = ColorUtils::tosRGBVec3(lcolor);
-            hfmMat.diffuseColor = dcolor;
-            hfmMat._material->setAlbedo(dcolor);
-            hfmMat._material->setOpacity(material.pbrMetallicRoughness.baseColorFactor[3]);
-        }
+
+        hfmMat._material->setRoughness(material.pbr_metallic_roughness.roughness_factor);
+
+        glm::vec3 lcolor = glm::vec3(material.pbr_metallic_roughness.base_color_factor[0],
+                                     material.pbr_metallic_roughness.base_color_factor[1],
+                                     material.pbr_metallic_roughness.base_color_factor[2]);
+        glm::vec3 dcolor = ColorUtils::tosRGBVec3(lcolor);
+        hfmMat.diffuseColor = dcolor;
+        hfmMat._material->setAlbedo(dcolor);
+        hfmMat._material->setOpacity(material.pbr_metallic_roughness.base_color_factor[3]);
     }
 
 }
 
-template<typename T, typename L>
-bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int count,
-                           QVector<L>& outarray, int accessorType, bool normalized) {
-
-    QDataStream blobstream(bin);
-    blobstream.setByteOrder(QDataStream::LittleEndian);
-    blobstream.setVersion(QDataStream::Qt_5_9);
-    blobstream.setFloatingPointPrecision(QDataStream::FloatingPointPrecision::SinglePrecision);
-    blobstream.skipRawData(byteOffset);
-
-    int bufferCount = 0;
-    switch (accessorType) {
-    case GLTFAccessorType::SCALAR:
-        bufferCount = 1;
-        break;
-    case GLTFAccessorType::VEC2:
-        bufferCount = 2;
-        break;
-    case GLTFAccessorType::VEC3:
-        bufferCount = 3;
-        break;
-    case GLTFAccessorType::VEC4:
-        bufferCount = 4;
-        break;
-    case GLTFAccessorType::MAT2:
-        bufferCount = 4;
-        break;
-    case GLTFAccessorType::MAT3:
-        bufferCount = 9;
-        break;
-    case GLTFAccessorType::MAT4:
-        bufferCount = 16;
-        break;
-    default:
-        qWarning(modelformat) << "Unknown accessorType: " << accessorType;
-        blobstream.setDevice(nullptr);
-        return false;
-    }
-
-    float scale = 1.0f;  // Normalized output values should always be floats.
-    if (normalized) {
-        scale = (float)(std::numeric_limits<T>::max)();
-    }
-
-    for (int i = 0; i < count; ++i) {
-        for (int j = 0; j < bufferCount; ++j) {
-            if (!blobstream.atEnd()) {
-                T value;
-                blobstream >> value;
-                if (normalized) {
-                    outarray.push_back(std::max((float)value / scale, -1.0f));
-                } else {
-                    outarray.push_back(value);
-                }
-            } else {
-                blobstream.setDevice(nullptr);
-                return false;
-            }
-        }
-    }
-
-    blobstream.setDevice(nullptr);
-    return true;
-}
-template<typename T>
-bool GLTFSerializer::addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count,
-                                QVector<T>& outarray, int accessorType, int componentType, bool normalized) {
-
-    switch (componentType) {
-    case GLTFAccessorComponentType::BYTE: {}
-    case GLTFAccessorComponentType::UNSIGNED_BYTE: {
-        return readArray<uchar>(bin, byteOffset, count, outarray, accessorType, normalized);
-    }
-    case GLTFAccessorComponentType::SHORT: {
-        return readArray<short>(bin, byteOffset, count, outarray, accessorType, normalized);
-    }
-    case GLTFAccessorComponentType::UNSIGNED_INT: {
-        return readArray<uint>(bin, byteOffset, count, outarray, accessorType, normalized);
-    }
-    case GLTFAccessorComponentType::UNSIGNED_SHORT: {
-        return readArray<ushort>(bin, byteOffset, count, outarray, accessorType, normalized);
-    }
-    case GLTFAccessorComponentType::FLOAT: {
-        return readArray<float>(bin, byteOffset, count, outarray, accessorType, normalized);
-    }
-    }
-    return false;
-}
-
-template <typename T>
-bool GLTFSerializer::addArrayFromAccessor(GLTFAccessor& accessor, QVector<T>& outarray) {
-    bool success = true;
-
-    if (accessor.defined["bufferView"]) {
-        GLTFBufferView& bufferview = _file.bufferviews[accessor.bufferView];
-        GLTFBuffer& buffer = _file.buffers[bufferview.buffer];
-
-        int accBoffset = accessor.defined["byteOffset"] ? accessor.byteOffset : 0;
-
-        success = addArrayOfType(buffer.blob, bufferview.byteOffset + accBoffset, accessor.count, outarray, accessor.type,
-                                 accessor.componentType, accessor.normalized);
-    } else {
-        for (int i = 0; i < accessor.count; ++i) {
-            T value;
-            memset(&value, 0, sizeof(T));  // Make sure the dummy array is initialized to zero.
-            outarray.push_back(value);
-        }
-    }
-
-    if (success) {
-        if (accessor.defined["sparse"]) {
-            QVector<int> out_sparse_indices_array;
-
-            GLTFBufferView& sparseIndicesBufferview = _file.bufferviews[accessor.sparse.indices.bufferView];
-            GLTFBuffer& sparseIndicesBuffer = _file.buffers[sparseIndicesBufferview.buffer];
-
-            int accSIBoffset = accessor.sparse.indices.defined["byteOffset"] ? accessor.sparse.indices.byteOffset : 0;
-
-            success = addArrayOfType(sparseIndicesBuffer.blob, sparseIndicesBufferview.byteOffset + accSIBoffset,
-                                     accessor.sparse.count, out_sparse_indices_array, GLTFAccessorType::SCALAR,
-                                     accessor.sparse.indices.componentType, false);
-            if (success) {
-                QVector<T> out_sparse_values_array;
-
-                GLTFBufferView& sparseValuesBufferview = _file.bufferviews[accessor.sparse.values.bufferView];
-                GLTFBuffer& sparseValuesBuffer = _file.buffers[sparseValuesBufferview.buffer];
-
-                int accSVBoffset = accessor.sparse.values.defined["byteOffset"] ? accessor.sparse.values.byteOffset : 0;
-
-                success = addArrayOfType(sparseValuesBuffer.blob, sparseValuesBufferview.byteOffset + accSVBoffset,
-                                         accessor.sparse.count, out_sparse_values_array, accessor.type, accessor.componentType,
-                                         accessor.normalized);
-
-                if (success) {
-                    for (int i = 0; i < accessor.sparse.count; ++i) {
-                        if ((i * 3) + 2 < out_sparse_values_array.size()) {
-                            if ((out_sparse_indices_array[i] * 3) + 2 < outarray.length()) {
-                                for (int j = 0; j < 3; ++j) {
-                                    outarray[(out_sparse_indices_array[i] * 3) + j] = out_sparse_values_array[(i * 3) + j];
-                                }
-                            } else {
-                                success = false;
-                                break;
-                            }
-                        } else {
-                            success = false;
-                            break;
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    return success;
-}
-
 void GLTFSerializer::retriangulate(const QVector<int>& inIndices, const QVector<glm::vec3>& in_vertices,
                                const QVector<glm::vec3>& in_normals, QVector<int>& outIndices,
                                QVector<glm::vec3>& out_vertices, QVector<glm::vec3>& out_normals) {
@@ -2123,29 +1546,6 @@ void GLTFSerializer::retriangulate(const QVector<int>& inIndices, const QVector<
     }
 }
 
-void GLTFSerializer::glTFDebugDump() {
-    qCDebug(modelformat) << "\n";
-    qCDebug(modelformat) << "---------------- GLTF Model ----------------";
-
-    qCDebug(modelformat) << "---------------- Nodes ----------------";
-    for (GLTFNode node : _file.nodes) {
-        if (node.defined["mesh"]) {
-            qCDebug(modelformat) << "    node_transforms" << node.transforms;
-        }
-    }
-
-    qCDebug(modelformat) << "---------------- Accessors ----------------";
-    for (GLTFAccessor accessor : _file.accessors) {
-        qCDebug(modelformat) << "count: " << accessor.count;
-        qCDebug(modelformat) << "byteOffset: " << accessor.byteOffset;
-    }
-
-    qCDebug(modelformat) << "---------------- Textures ----------------";
-    for (GLTFTexture texture : _file.textures) {
-        if (texture.defined["source"]) {
-            QString url = _file.images[texture.source].uri;
-            QString fname = hifi::URL(url).fileName();
-            qCDebug(modelformat) << "fname: " << fname;
-        }
-    }
-}
+GLTFSerializer::~GLTFSerializer() {
+    cgltf_free(_data);
+}
\ No newline at end of file
diff --git a/libraries/model-serializers/src/GLTFSerializer.h b/libraries/model-serializers/src/GLTFSerializer.h
index da5284d0b3..18b3396c45 100644
--- a/libraries/model-serializers/src/GLTFSerializer.h
+++ b/libraries/model-serializers/src/GLTFSerializer.h
@@ -4,7 +4,7 @@
 //
 //  Created by Luis Cuenca on 8/30/17.
 //  Copyright 2017 High Fidelity, Inc.
-//  Copyright 2023 Overte e.V.
+//  Copyright 2023-2024 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
@@ -19,748 +19,12 @@
 #include <hfm/ModelFormatLogging.h>
 #include <hfm/HFMSerializer.h>
 
+float atof_locale_independent(char* str);
 
-struct GLTFAsset {
-    QString generator;
-    QString version; //required
-    QString copyright;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["generator"]) {
-            qCDebug(modelformat) << "generator: " << generator;
-        }
-        if (defined["version"]) {
-            qCDebug(modelformat) << "version: " << version;
-        }
-        if (defined["copyright"]) {
-            qCDebug(modelformat) << "copyright: " << copyright;
-        }
-    }
-};
+#define CGLTF_ATOF(str) atof_locale_independent(str)
 
-struct GLTFNode {
-    QString name;
-    int camera;
-    int mesh;
-    QVector<int> children;
-    QVector<double> translation;
-    QVector<double> rotation;
-    QVector<double> scale;
-    QVector<double> matrix;
-    QVector<glm::mat4> transforms;
-    int skin;
-    QVector<int> skeletons;
-    QString jointName;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["name"]) {
-            qCDebug(modelformat) << "name: " << name;
-        }
-        if (defined["camera"]) {
-            qCDebug(modelformat) << "camera: " << camera;
-        }
-        if (defined["mesh"]) {
-            qCDebug(modelformat) << "mesh: " << mesh;
-        }
-        if (defined["skin"]) {
-            qCDebug(modelformat) << "skin: " << skin;
-        }
-        if (defined["jointName"]) {
-            qCDebug(modelformat) << "jointName: " << jointName;
-        }
-        if (defined["children"]) {
-            qCDebug(modelformat) << "children: " << children;
-        }
-        if (defined["translation"]) {
-            qCDebug(modelformat) << "translation: " << translation;
-        }
-        if (defined["rotation"]) {
-            qCDebug(modelformat) << "rotation: " << rotation;
-        }
-        if (defined["scale"]) {
-            qCDebug(modelformat) << "scale: " << scale;
-        }
-        if (defined["matrix"]) {
-            qCDebug(modelformat) << "matrix: " << matrix;
-        }
-        if (defined["skeletons"]) {
-            qCDebug(modelformat) << "skeletons: " << skeletons;
-        }
-    }
-};
+#include "cgltf.h"
 
-// Meshes
-
-struct GLTFMeshPrimitivesTarget {
-    int normal;
-    int position;
-    int tangent;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["normal"]) {
-            qCDebug(modelformat) << "normal: " << normal;
-        }
-        if (defined["position"]) {
-            qCDebug(modelformat) << "position: " << position;
-        }
-        if (defined["tangent"]) {
-            qCDebug(modelformat) << "tangent: " << tangent;
-        }
-    }
-};
-
-namespace GLTFMeshPrimitivesRenderingMode {
-    enum Values {
-        POINTS = 0,
-        LINES,
-        LINE_LOOP,
-        LINE_STRIP,
-        TRIANGLES,
-        TRIANGLE_STRIP,
-        TRIANGLE_FAN
-    };
-}
-
-struct GLTFMeshPrimitiveAttr {
-    QMap<QString, int> values;
-    QMap<QString, bool> defined;
-    void dump() {
-        QList<QString> keys = values.keys();
-        qCDebug(modelformat) << "values: ";
-        foreach(auto k, keys) {
-            qCDebug(modelformat) << k << ": " << values[k];
-        }
-    }
-};
-
-struct GLTFMeshPrimitive {
-    GLTFMeshPrimitiveAttr attributes;
-    int indices;
-    int material;
-    int mode{ GLTFMeshPrimitivesRenderingMode::TRIANGLES };
-    QVector<GLTFMeshPrimitiveAttr> targets;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["attributes"]) {
-            qCDebug(modelformat) << "attributes: ";
-            attributes.dump();
-        }
-        if (defined["indices"]) {
-            qCDebug(modelformat) << "indices: " << indices;
-        }
-        if (defined["material"]) {
-            qCDebug(modelformat) << "material: " << material;
-        }
-        if (defined["mode"]) {
-            qCDebug(modelformat) << "mode: " << mode;
-        }
-        if (defined["targets"]) {
-            qCDebug(modelformat) << "targets: ";
-            foreach(auto t, targets) t.dump();
-        }
-    }
-};
-
-struct GLTFMeshExtra {
-    QVector<QString> targetNames;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["targetNames"]) {
-            qCDebug(modelformat) << "targetNames: " << targetNames;
-        }
-    }
-};
-
-struct GLTFMesh {
-    QString name;
-    QVector<GLTFMeshPrimitive> primitives;
-    GLTFMeshExtra extras;
-    QVector<double> weights;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["name"]) {
-            qCDebug(modelformat) << "name: " << name;
-        }
-        if (defined["primitives"]) {
-            qCDebug(modelformat) << "primitives: ";
-            foreach(auto prim, primitives) prim.dump();
-        }
-        if (defined["extras"]) {
-            qCDebug(modelformat) << "extras: ";
-            extras.dump();
-        }
-        if (defined["weights"]) {
-            qCDebug(modelformat) << "weights: " << weights;
-        }
-    }
-};
-
-// BufferViews
-
-namespace GLTFBufferViewTarget {
-    enum Values {
-        ARRAY_BUFFER = 34962,
-        ELEMENT_ARRAY_BUFFER = 34963
-    };
-}
-
-struct GLTFBufferView {
-    int buffer; //required
-    int byteLength; //required
-    int byteOffset { 0 };
-    int target;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["buffer"]) {
-            qCDebug(modelformat) << "buffer: " << buffer;
-        }
-        if (defined["byteLength"]) {
-            qCDebug(modelformat) << "byteLength: " << byteLength;
-        }
-        if (defined["byteOffset"]) {
-            qCDebug(modelformat) << "byteOffset: " << byteOffset;
-        }
-        if (defined["target"]) {
-            qCDebug(modelformat) << "target: " << target;
-        }
-    }
-};
-
-// Buffers
-
-struct GLTFBuffer {
-    int byteLength; //required
-    QString uri;
-    hifi::ByteArray blob;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["byteLength"]) {
-            qCDebug(modelformat) << "byteLength: " << byteLength;
-        }
-        if (defined["uri"]) {
-            qCDebug(modelformat) << "uri: " << uri;
-        }
-        if (defined["blob"]) {
-            qCDebug(modelformat) << "blob: " << "DEFINED";
-        }
-    }
-};
-
-// Samplers
-namespace GLTFSamplerFilterType {
-    enum Values {
-        NEAREST = 9728,
-        LINEAR = 9729,
-        NEAREST_MIPMAP_NEAREST = 9984,
-        LINEAR_MIPMAP_NEAREST = 9985,
-        NEAREST_MIPMAP_LINEAR = 9986,
-        LINEAR_MIPMAP_LINEAR = 9987
-    };
-}
-
-namespace GLTFSamplerWrapType {
-    enum Values {
-        CLAMP_TO_EDGE = 33071,
-        MIRRORED_REPEAT = 33648,
-        REPEAT = 10497
-    };
-}
-
-struct GLTFSampler {
-    int magFilter;
-    int minFilter;
-    int wrapS;
-    int wrapT;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["magFilter"]) {
-            qCDebug(modelformat) << "magFilter: " << magFilter;
-        }
-        if (defined["minFilter"]) {
-            qCDebug(modelformat) << "minFilter: " << minFilter;
-        }
-        if (defined["wrapS"]) {
-            qCDebug(modelformat) << "wrapS: " << wrapS;
-        }
-        if (defined["wrapT"]) {
-            qCDebug(modelformat) << "wrapT: " << wrapT;
-        }
-    }
-};
-
-// Cameras
-
-struct GLTFCameraPerspective {
-    double aspectRatio;
-    double yfov; //required
-    double zfar;
-    double znear; //required
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["zfar"]) {
-            qCDebug(modelformat) << "zfar: " << zfar;
-        }
-        if (defined["znear"]) {
-            qCDebug(modelformat) << "znear: " << znear;
-        }
-        if (defined["aspectRatio"]) {
-            qCDebug(modelformat) << "aspectRatio: " << aspectRatio;
-        }
-        if (defined["yfov"]) {
-            qCDebug(modelformat) << "yfov: " << yfov;
-        }
-    }
-};
-
-struct GLTFCameraOrthographic {
-    double zfar; //required
-    double znear; //required
-    double xmag; //required
-    double ymag; //required
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["zfar"]) {
-            qCDebug(modelformat) << "zfar: " << zfar;
-        }
-        if (defined["znear"]) {
-            qCDebug(modelformat) << "znear: " << znear;
-        }
-        if (defined["xmag"]) {
-            qCDebug(modelformat) << "xmag: " << xmag;
-        }
-        if (defined["ymag"]) {
-            qCDebug(modelformat) << "ymag: " << ymag;
-        }
-    }
-};
-
-namespace GLTFCameraTypes {
-    enum Values {
-        ORTHOGRAPHIC = 0,
-        PERSPECTIVE
-    };
-}
-
-struct GLTFCamera {
-    QString name;
-    GLTFCameraPerspective perspective;  //required (or)
-    GLTFCameraOrthographic orthographic;  //required (or)
-    int type;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["name"]) {
-            qCDebug(modelformat) << "name: " << name;
-        }
-        if (defined["type"]) {
-            qCDebug(modelformat) << "type: " << type;
-        }
-        if (defined["perspective"]) {
-            perspective.dump();
-        }
-        if (defined["orthographic"]) {
-            orthographic.dump();
-        }
-    }
-};
-
-// Images
-
-namespace GLTFImageMimetype {
-    enum Values {
-        JPEG = 0,
-        PNG
-    };
-};
-
-struct GLTFImage {
-    QString uri;  //required (or)
-    int mimeType;
-    int bufferView;   //required (or)
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["mimeType"]) {
-            qCDebug(modelformat) << "mimeType: " << mimeType;
-        }
-        if (defined["bufferView"]) {
-            qCDebug(modelformat) << "bufferView: " << bufferView;
-        }
-    }
-};
-
-// Materials
-
-struct GLTFpbrMetallicRoughness {
-    QVector<double> baseColorFactor;
-    int baseColorTexture;
-    int metallicRoughnessTexture;
-    double metallicFactor;
-    double roughnessFactor;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["baseColorFactor"]) {
-            qCDebug(modelformat) << "baseColorFactor: " << baseColorFactor;
-        }
-        if (defined["baseColorTexture"]) {
-            qCDebug(modelformat) << "baseColorTexture: " << baseColorTexture;
-        }
-        if (defined["metallicRoughnessTexture"]) {
-            qCDebug(modelformat) << "metallicRoughnessTexture: " << metallicRoughnessTexture;
-        }
-        if (defined["metallicFactor"]) {
-            qCDebug(modelformat) << "metallicFactor: " << metallicFactor;
-        }
-        if (defined["roughnessFactor"]) {
-            qCDebug(modelformat) << "roughnessFactor: " << roughnessFactor;
-        }
-        if (defined["baseColorFactor"]) {
-            qCDebug(modelformat) << "baseColorFactor: " << baseColorFactor;
-        }
-    }
-};
-
-struct GLTFMaterial {
-    QString name;
-    QVector<double> emissiveFactor;
-    int emissiveTexture;
-    int normalTexture;
-    int occlusionTexture;
-    graphics::MaterialKey::OpacityMapMode alphaMode { graphics::MaterialKey::OPACITY_MAP_OPAQUE };
-    double alphaCutoff;
-    bool doubleSided { false };
-    GLTFpbrMetallicRoughness pbrMetallicRoughness;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["name"]) {
-            qCDebug(modelformat) << "name: " << name;
-        }
-        if (defined["emissiveTexture"]) {
-            qCDebug(modelformat) << "emissiveTexture: " << emissiveTexture;
-        }
-        if (defined["normalTexture"]) {
-            qCDebug(modelformat) << "normalTexture: " << normalTexture;
-        }
-        if (defined["occlusionTexture"]) {
-            qCDebug(modelformat) << "occlusionTexture: " << occlusionTexture;
-        }
-        if (defined["emissiveFactor"]) {
-            qCDebug(modelformat) << "emissiveFactor: " << emissiveFactor;
-        }
-        if (defined["alphaMode"]) {
-            qCDebug(modelformat) << "alphaMode: " << alphaMode;
-        }
-        if (defined["alphaCutoff"]) {
-            qCDebug(modelformat) << "alphaCutoff: " << alphaCutoff;
-        }
-        if (defined["pbrMetallicRoughness"]) {
-            pbrMetallicRoughness.dump();
-        }
-    }
-};
-
-// Accesors
-
-namespace GLTFAccessorType {
-    enum Values {
-        SCALAR = 0,
-        VEC2,
-        VEC3,
-        VEC4,
-        MAT2,
-        MAT3,
-        MAT4
-    };
-}
-namespace GLTFAccessorComponentType {
-    enum Values {
-        BYTE = 5120,
-        UNSIGNED_BYTE = 5121,
-        SHORT = 5122,
-        UNSIGNED_SHORT = 5123,
-        UNSIGNED_INT = 5125,
-        FLOAT = 5126
-    };
-}
-struct GLTFAccessor {
-    struct GLTFAccessorSparse {
-        struct GLTFAccessorSparseIndices {
-            int bufferView;
-            int byteOffset{ 0 };
-            int componentType;
-
-            QMap<QString, bool> defined;
-            void dump() {
-                if (defined["bufferView"]) {
-                    qCDebug(modelformat) << "bufferView: " << bufferView;
-                }
-                if (defined["byteOffset"]) {
-                    qCDebug(modelformat) << "byteOffset: " << byteOffset;
-                }
-                if (defined["componentType"]) {
-                    qCDebug(modelformat) << "componentType: " << componentType;
-                }
-            }
-        };
-        struct GLTFAccessorSparseValues {
-            int bufferView;
-            int byteOffset{ 0 };
-
-            QMap<QString, bool> defined;
-            void dump() {
-                if (defined["bufferView"]) {
-                    qCDebug(modelformat) << "bufferView: " << bufferView;
-                }
-                if (defined["byteOffset"]) {
-                    qCDebug(modelformat) << "byteOffset: " << byteOffset;
-                }
-            }
-        };
-
-        int count;
-        GLTFAccessorSparseIndices indices;
-        GLTFAccessorSparseValues values;
-
-        QMap<QString, bool> defined;
-        void dump() {
-
-        }
-    };
-    int bufferView;
-    int byteOffset { 0 };
-    int componentType; //required
-    int count; //required
-    int type; //required
-    bool normalized { false };
-    QVector<double> max;
-    QVector<double> min;
-    GLTFAccessorSparse sparse;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["bufferView"]) {
-            qCDebug(modelformat) << "bufferView: " << bufferView;
-        }
-        if (defined["byteOffset"]) {
-            qCDebug(modelformat) << "byteOffset: " << byteOffset;
-        }
-        if (defined["componentType"]) {
-            qCDebug(modelformat) << "componentType: " << componentType;
-        }
-        if (defined["count"]) {
-            qCDebug(modelformat) << "count: " << count;
-        }
-        if (defined["type"]) {
-            qCDebug(modelformat) << "type: " << type;
-        }
-        if (defined["normalized"]) {
-            qCDebug(modelformat) << "normalized: " << (normalized ? "TRUE" : "FALSE");
-        }
-        if (defined["max"]) {
-            qCDebug(modelformat) << "max: ";
-            foreach(float m, max) {
-                qCDebug(modelformat) << m;
-            }
-        }
-        if (defined["min"]) {
-            qCDebug(modelformat) << "min: ";
-            foreach(float m, min) {
-                qCDebug(modelformat) << m;
-            }
-        }
-        if (defined["sparse"]) {
-            qCDebug(modelformat) << "sparse: ";
-            sparse.dump();
-        }
-    }
-};
-
-// Animation
-
-namespace GLTFChannelTargetPath {
-    enum Values {
-        TRANSLATION = 0,
-        ROTATION,
-        SCALE
-    };
-}
-
-struct GLTFChannelTarget {
-    int node;
-    int path;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["node"]) {
-            qCDebug(modelformat) << "node: " << node;
-        }
-        if (defined["path"]) {
-            qCDebug(modelformat) << "path: " << path;
-        }
-    }
-};
-
-struct GLTFChannel {
-    int sampler;
-    GLTFChannelTarget target;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["sampler"]) {
-            qCDebug(modelformat) << "sampler: " << sampler;
-        }
-        if (defined["target"]) {
-            target.dump();
-        }
-    }
-};
-
-namespace GLTFAnimationSamplerInterpolation {
-    enum Values{
-        LINEAR = 0
-    };
-}
-
-struct GLTFAnimationSampler {
-    int input;
-    int output;
-    int interpolation;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["input"]) {
-            qCDebug(modelformat) << "input: " << input;
-        }
-        if (defined["output"]) {
-            qCDebug(modelformat) << "output: " << output;
-        }
-        if (defined["interpolation"]) {
-            qCDebug(modelformat) << "interpolation: " << interpolation;
-        }
-    }
-};
-
-struct GLTFAnimation {
-    QVector<GLTFChannel> channels;
-    QVector<GLTFAnimationSampler> samplers;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["channels"]) {
-            foreach(auto channel, channels) channel.dump();
-        }
-        if (defined["samplers"]) {
-            foreach(auto sampler, samplers) sampler.dump();
-        }
-    }
-};
-
-struct GLTFScene {
-    QString name;
-    QVector<int> nodes;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["name"]) {
-            qCDebug(modelformat) << "name: " << name;
-        }
-        if (defined["nodes"]) {
-            qCDebug(modelformat) << "nodes: ";
-            foreach(int node, nodes) qCDebug(modelformat) << node;
-        }
-    }
-};
-
-struct GLTFSkin {
-    int inverseBindMatrices;
-    QVector<int> joints;
-    int skeleton;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["inverseBindMatrices"]) {
-            qCDebug(modelformat) << "inverseBindMatrices: " << inverseBindMatrices;
-        }
-        if (defined["skeleton"]) {
-            qCDebug(modelformat) << "skeleton: " << skeleton;
-        }
-        if (defined["joints"]) {
-            qCDebug(modelformat) << "joints: ";
-            foreach(int joint, joints) qCDebug(modelformat) << joint;
-        }
-    }
-};
-
-struct GLTFTexture {
-    int sampler;
-    int source;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["sampler"]) {
-            qCDebug(modelformat) << "sampler: " << sampler;
-        }
-        if (defined["source"]) {
-            qCDebug(modelformat) << "source: " << sampler;
-        }
-    }
-};
-
-struct GLTFFile {
-    GLTFAsset asset;
-    int scene = 0;
-    QVector<GLTFAccessor> accessors;
-    QVector<GLTFAnimation> animations;
-    QVector<GLTFBufferView> bufferviews;
-    QVector<GLTFBuffer> buffers;
-    QVector<GLTFCamera> cameras;
-    QVector<GLTFImage> images;
-    QVector<GLTFMaterial> materials;
-    QVector<GLTFMesh> meshes;
-    QVector<GLTFNode> nodes;
-    QVector<GLTFSampler> samplers;
-    QVector<GLTFScene> scenes;
-    QVector<GLTFSkin> skins;
-    QVector<GLTFTexture> textures;
-    QMap<QString, bool> defined;
-    void dump() {
-        if (defined["asset"]) {
-            asset.dump();
-        }
-        if (defined["scene"]) {
-            qCDebug(modelformat) << "scene: " << scene;
-        }
-        if (defined["accessors"]) {
-            foreach(auto acc, accessors) acc.dump();
-        }
-        if (defined["animations"]) {
-            foreach(auto ani, animations) ani.dump();
-        }
-        if (defined["bufferviews"]) {
-            foreach(auto bv, bufferviews) bv.dump();
-        }
-        if (defined["buffers"]) {
-            foreach(auto b, buffers) b.dump();
-        }
-        if (defined["cameras"]) {
-            foreach(auto c, cameras) c.dump();
-        }
-        if (defined["images"]) {
-            foreach(auto i, images) i.dump();
-        }
-        if (defined["materials"]) {
-            foreach(auto mat, materials) mat.dump();
-        }
-        if (defined["meshes"]) {
-            foreach(auto mes, meshes) mes.dump();
-        }
-        if (defined["nodes"]) {
-            foreach(auto nod, nodes) nod.dump();
-        }
-        if (defined["samplers"]) {
-            foreach(auto sa, samplers) sa.dump();
-        }
-        if (defined["scenes"]) {
-            foreach(auto sc, scenes) sc.dump();
-        }
-        if (defined["skins"]) {
-            foreach(auto sk, nodes) sk.dump();
-        }
-        if (defined["textures"]) {
-            foreach(auto tex, textures) tex.dump();
-        }
-    }
-};
 
 class GLTFSerializer : public QObject, public HFMSerializer {
     Q_OBJECT
@@ -769,79 +33,19 @@ public:
     std::unique_ptr<hfm::Serializer::Factory> getFactory() const override;
 
     HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override;
+    ~GLTFSerializer();
 private:
-    GLTFFile _file;
+    cgltf_data* _data {nullptr};
     hifi::URL _url;
-    hifi::ByteArray _glbBinary;
+    QVector<hifi::ByteArray> _externalData;
 
-    glm::mat4 getModelTransform(const GLTFNode& node);
-    void getSkinInverseBindMatrices(std::vector<std::vector<float>>& inverseBindMatrixValues);
-    void generateTargetData(int index, float weight, QVector<glm::vec3>& returnVector);
+    glm::mat4 getModelTransform(const cgltf_node& node);
+    bool getSkinInverseBindMatrices(std::vector<std::vector<float>>& inverseBindMatrixValues);
+    bool generateTargetData(cgltf_accessor *accessor, float weight, QVector<glm::vec3>& returnVector);
 
     bool buildGeometry(HFMModel& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& url);
-    bool parseGLTF(const hifi::ByteArray& data);
 
-    bool getStringVal(const QJsonObject& object, const QString& fieldname,
-                      QString& value, QMap<QString, bool>&  defined);
-    bool getBoolVal(const QJsonObject& object, const QString& fieldname,
-                    bool& value, QMap<QString, bool>&  defined);
-    bool getIntVal(const QJsonObject& object, const QString& fieldname,
-                   int& value, QMap<QString, bool>&  defined);
-    bool getDoubleVal(const QJsonObject& object, const QString& fieldname,
-                      double& value, QMap<QString, bool>&  defined);
-    bool getObjectVal(const QJsonObject& object, const QString& fieldname,
-                      QJsonObject& value, QMap<QString, bool>&  defined);
-    bool getIntArrayVal(const QJsonObject& object, const QString& fieldname,
-                        QVector<int>& values, QMap<QString, bool>&  defined);
-    bool getDoubleArrayVal(const QJsonObject& object, const QString& fieldname,
-                           QVector<double>& values, QMap<QString, bool>&  defined);
-    bool getObjectArrayVal(const QJsonObject& object, const QString& fieldname,
-                           QJsonArray& objects, QMap<QString, bool>& defined);
-
-    hifi::ByteArray setGLBChunks(const hifi::ByteArray& data);
-
-    graphics::MaterialKey::OpacityMapMode getMaterialAlphaMode(const QString& type);
-    int getAccessorType(const QString& type);
-    int getAnimationSamplerInterpolation(const QString& interpolation);
-    int getCameraType(const QString& type);
-    int getImageMimeType(const QString& mime);
-    int getMeshPrimitiveRenderingMode(const QString& type);
-
-    bool getIndexFromObject(const QJsonObject& object, const QString& field,
-                            int& outidx, QMap<QString, bool>& defined);
-
-    bool setAsset(const QJsonObject& object);
-
-    GLTFAccessor::GLTFAccessorSparse::GLTFAccessorSparseIndices createAccessorSparseIndices(const QJsonObject& object);
-    GLTFAccessor::GLTFAccessorSparse::GLTFAccessorSparseValues createAccessorSparseValues(const QJsonObject& object);
-    GLTFAccessor::GLTFAccessorSparse createAccessorSparse(const QJsonObject& object);
-
-    bool addAccessor(const QJsonObject& object);
-    bool addAnimation(const QJsonObject& object);
-    bool addBufferView(const QJsonObject& object);
-    bool addBuffer(const QJsonObject& object);
-    bool addCamera(const QJsonObject& object);
-    bool addImage(const QJsonObject& object);
-    bool addMaterial(const QJsonObject& object);
-    bool addMesh(const QJsonObject& object);
-    bool addNode(const QJsonObject& object);
-    bool addSampler(const QJsonObject& object);
-    bool addScene(const QJsonObject& object);
-    bool addSkin(const QJsonObject& object);
-    bool addTexture(const QJsonObject& object);
-
-    bool readBinary(const QString& url, hifi::ByteArray& outdata);
-
-    template<typename T, typename L>
-    bool readArray(const hifi::ByteArray& bin, int byteOffset, int count,
-                   QVector<L>& outarray, int accessorType, bool normalized);
-
-    template<typename T>
-    bool addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count,
-                        QVector<T>& outarray, int accessorType, int componentType, bool normalized);
-
-    template <typename T>
-    bool addArrayFromAccessor(GLTFAccessor& accessor, QVector<T>& outarray);
+    bool readBinary(const QString& url, cgltf_buffer &buffer);
 
     void retriangulate(const QVector<int>& in_indices, const QVector<glm::vec3>& in_vertices,
                        const QVector<glm::vec3>& in_normals, QVector<int>& out_indices,
@@ -851,12 +55,9 @@ private:
     hifi::ByteArray requestEmbeddedData(const QString& url);
 
     QNetworkReply* request(hifi::URL& url, bool isTest);
-    bool doesResourceExist(const QString& url);
 
-    void setHFMMaterial(HFMMaterial& hfmMat, const GLTFMaterial& material);
-    HFMTexture getHFMTexture(const GLTFTexture& texture);
-
-    void glTFDebugDump();
+    void setHFMMaterial(HFMMaterial& hfmMat, const cgltf_material& material);
+    HFMTexture getHFMTexture(const cgltf_texture *texture);
 };
 
 #endif // hifi_GLTFSerializer_h
diff --git a/libraries/networking/src/FingerprintUtils.cpp b/libraries/networking/src/FingerprintUtils.cpp
index cab8cde832..5bb530d332 100644
--- a/libraries/networking/src/FingerprintUtils.cpp
+++ b/libraries/networking/src/FingerprintUtils.cpp
@@ -42,7 +42,7 @@ static const int HASH_ITERATIONS = 65535;
 
 // Salt string for the hardware ID, makes our IDs unique to our project.
 // Changing this results in different hardware IDs being computed.
-static const QByteArray HASH_SALT{"Vircadia"};
+static const QByteArray HASH_SALT{"Overte"};
 
 static const QString FALLBACK_FINGERPRINT_KEY = "fallbackFingerprint";
 
diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp
index e4d3f0a207..d182e7ea94 100644
--- a/libraries/networking/src/LimitedNodeList.cpp
+++ b/libraries/networking/src/LimitedNodeList.cpp
@@ -41,7 +41,7 @@
 
 #if defined(Q_OS_WIN)
 #include <winsock.h>
-#else 
+#else
 #include <arpa/inet.h>
 #endif
 
@@ -197,6 +197,10 @@ void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) {
         newPermissions.can(NodePermissions::Permission::canRezAvatarEntities)) {
         emit canRezAvatarEntitiesChanged(_permissions.can(NodePermissions::Permission::canRezAvatarEntities));
     }
+    if (originalPermissions.can(NodePermissions::Permission::canViewAssetURLs) !=
+        newPermissions.can(NodePermissions::Permission::canViewAssetURLs)) {
+        emit canViewAssetURLsChanged(_permissions.can(NodePermissions::Permission::canViewAssetURLs));
+    }
 }
 
 void LimitedNodeList::setSocketLocalPort(SocketType socketType, quint16 socketLocalPort) {
@@ -497,7 +501,7 @@ qint64 LimitedNodeList::sendUnreliableUnorderedPacketList(NLPacketList& packetLi
         }
         return bytesSent;
     } else {
-        qCDebug(networking) << "LimitedNodeList::sendUnreliableUnorderedPacketList called without active socket for node" 
+        qCDebug(networking) << "LimitedNodeList::sendUnreliableUnorderedPacketList called without active socket for node"
             << destinationNode << " - not sending.";
         return ERROR_SENDING_PACKET_BYTES;
     }
@@ -1139,7 +1143,7 @@ void LimitedNodeList::startSTUNPublicSocketUpdate() {
         if (_stunSockAddr.getAddress().isNull()) {
 
             // if we fail to lookup the socket then timeout the STUN address lookup
-            connect(&_stunSockAddr, &SockAddr::lookupFailed, this, &LimitedNodeList::possiblyTimeoutSTUNAddressLookup);
+            connect(&_stunSockAddr, &SockAddr::lookupFailed, this, &LimitedNodeList::STUNAddressLookupFailed);
 
             // immediately send a STUN request once we know the socket
             connect(&_stunSockAddr, &SockAddr::lookupCompleted, this, &LimitedNodeList::sendSTUNRequest);
@@ -1153,7 +1157,7 @@ void LimitedNodeList::startSTUNPublicSocketUpdate() {
             QTimer* lookupTimeoutTimer = new QTimer { this };
             lookupTimeoutTimer->setSingleShot(true);
 
-            connect(lookupTimeoutTimer, &QTimer::timeout, this, &LimitedNodeList::possiblyTimeoutSTUNAddressLookup);
+            connect(lookupTimeoutTimer, &QTimer::timeout, this, &LimitedNodeList::STUNAddressLookupTimeout);
 
             // delete the lookup timeout timer once it has fired
             connect(lookupTimeoutTimer, &QTimer::timeout, lookupTimeoutTimer, &QTimer::deleteLater);
@@ -1168,10 +1172,18 @@ void LimitedNodeList::startSTUNPublicSocketUpdate() {
     }
 }
 
-void LimitedNodeList::possiblyTimeoutSTUNAddressLookup() {
+void LimitedNodeList::STUNAddressLookupFailed() {
+    if (_stunSockAddr.getAddress().isNull()) {
+        // got a lookup failure
+        qCCritical(networking) << "PAGE: Failed to lookup address of STUN server" << STUN_SERVER_HOSTNAME;
+        stopInitialSTUNUpdate(false);
+    }
+}
+
+void LimitedNodeList::STUNAddressLookupTimeout() {
     if (_stunSockAddr.getAddress().isNull()) {
         // our stun address is still NULL, but we've been waiting for long enough - time to force a fail
-        qCCritical(networking) << "PAGE: Failed to lookup address of STUN server" << STUN_SERVER_HOSTNAME;
+        qCCritical(networking) << "PAGE: Address lookup of STUN server" << STUN_SERVER_HOSTNAME << "timed out";
         stopInitialSTUNUpdate(false);
     }
 }
diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h
index 0634538880..cdb742a3c3 100644
--- a/libraries/networking/src/LimitedNodeList.h
+++ b/libraries/networking/src/LimitedNodeList.h
@@ -132,6 +132,7 @@ public:
     bool getThisNodeCanReplaceContent() const { return _permissions.can(NodePermissions::Permission::canReplaceDomainContent); }
     bool getThisNodeCanGetAndSetPrivateUserData() const { return _permissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData); }
     bool getThisNodeCanRezAvatarEntities() const { return _permissions.can(NodePermissions::Permission::canRezAvatarEntities); }
+    bool getThisNodeCanViewAssetURLs() const { return _permissions.can(NodePermissions::Permission::canViewAssetURLs); }
 
     quint16 getSocketLocalPort(SocketType socketType) const { return _nodeSocket.localPort(socketType); }
     Q_INVOKABLE void setSocketLocalPort(SocketType socketType, quint16 socketLocalPort);
@@ -392,6 +393,7 @@ signals:
     void canReplaceContentChanged(bool canReplaceContent);
     void canGetAndSetPrivateUserDataChanged(bool canGetAndSetPrivateUserData);
     void canRezAvatarEntitiesChanged(bool canRezAvatarEntities);
+    void canViewAssetURLsChanged(bool canViewAssetURLs);
 
 protected slots:
     void connectedForLocalSocketTest();
@@ -481,7 +483,8 @@ protected:
 
 private slots:
     void flagTimeForConnectionStep(ConnectionStep connectionStep, quint64 timestamp);
-    void possiblyTimeoutSTUNAddressLookup();
+    void STUNAddressLookupTimeout();
+    void STUNAddressLookupFailed();
     void addSTUNHandlerToUnfiltered(); // called once STUN socket known
 
 private:
diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h
index 4287da92d4..b10e09497a 100644
--- a/libraries/networking/src/NetworkingConstants.h
+++ b/libraries/networking/src/NetworkingConstants.h
@@ -58,6 +58,8 @@ namespace NetworkingConstants {
     const QString HF_PUBLIC_CDN_URL = "";
     const QString HF_MARKETPLACE_CDN_HOSTNAME = "";
     const QString OVERTE_CONTENT_CDN_URL = "https://content.overte.org/";
+    const QString OVERTE_COMMUNITY_APPLICATIONS = { "https://more.overte.org/applications" };
+    const QString OVERTE_TUTORIAL_SCRIPTS = { "https://more.overte.org/tutorial" };
 
 #if USE_STABLE_GLOBAL_SERVICES
     const QString ICE_SERVER_DEFAULT_HOSTNAME = "ice.overte.org";
diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h
index cc056ea7d0..92a4c424c8 100644
--- a/libraries/networking/src/Node.h
+++ b/libraries/networking/src/Node.h
@@ -84,6 +84,7 @@ public:
     bool getCanReplaceContent() const { return _permissions.can(NodePermissions::Permission::canReplaceDomainContent); }
     bool getCanGetAndSetPrivateUserData() const { return _permissions.can(NodePermissions::Permission::canGetAndSetPrivateUserData); }
     bool getCanRezAvatarEntities() const { return _permissions.can(NodePermissions::Permission::canRezAvatarEntities); }
+    bool getCanViewAssetURLs() const { return _permissions.can(NodePermissions::Permission::canViewAssetURLs); }
 
     using NodesIgnoredPair = std::pair<std::vector<QUuid>, bool>;
 
diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp
index e7cf953645..8a23c7dbd3 100644
--- a/libraries/networking/src/NodePermissions.cpp
+++ b/libraries/networking/src/NodePermissions.cpp
@@ -68,6 +68,7 @@ NodePermissions::NodePermissions(QMap<QString, QVariant> perms) {
     permissions |= perms["id_can_replace_content"].toBool() ? Permission::canReplaceDomainContent : Permission::none;
     permissions |= perms["id_can_get_and_set_private_user_data"].toBool() ? 
         Permission::canGetAndSetPrivateUserData : Permission::none;
+    permissions |= perms["id_can_view_asset_urls"].toBool() ? Permission::canViewAssetURLs : Permission::none;
 }
 
 QVariant NodePermissions::toVariant(QHash<QUuid, GroupRank> groupRanks) {
@@ -95,6 +96,7 @@ QVariant NodePermissions::toVariant(QHash<QUuid, GroupRank> groupRanks) {
     values["id_can_kick"] = can(Permission::canKick);
     values["id_can_replace_content"] = can(Permission::canReplaceDomainContent);
     values["id_can_get_and_set_private_user_data"] = can(Permission::canGetAndSetPrivateUserData);
+    values["id_can_view_asset_urls"] = can(Permission::canViewAssetURLs);
     return QVariant(values);
 }
 
@@ -167,6 +169,9 @@ QDebug operator<<(QDebug debug, const NodePermissions& perms) {
     if (perms.can(NodePermissions::Permission::canGetAndSetPrivateUserData)) {
         debug << " get-and-set-private-user-data";
     }
+    if (perms.can(NodePermissions::Permission::canViewAssetURLs)) {
+        debug << " can-view-asset-urls";
+    }
     debug.nospace() << "]";
     return debug.nospace();
 }
diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h
index 29baf130e6..81eaf68457 100644
--- a/libraries/networking/src/NodePermissions.h
+++ b/libraries/networking/src/NodePermissions.h
@@ -79,7 +79,8 @@ public:
         canKick = 64,
         canReplaceDomainContent = 128,
         canGetAndSetPrivateUserData = 1024,
-        canRezAvatarEntities = 2048
+        canRezAvatarEntities = 2048,
+        canViewAssetURLs = 4096
     };
     Q_DECLARE_FLAGS(Permissions, Permission)
     Permissions permissions;
diff --git a/libraries/networking/src/SockAddr.cpp b/libraries/networking/src/SockAddr.cpp
index 29fbde3934..1f049d14c7 100644
--- a/libraries/networking/src/SockAddr.cpp
+++ b/libraries/networking/src/SockAddr.cpp
@@ -67,12 +67,13 @@ SockAddr::SockAddr(SocketType socketType, const QString& hostname, quint16 hostO
     if (_address.protocol() != QAbstractSocket::IPv4Protocol) {
         // lookup the IP by the hostname
         if (shouldBlockForLookup) {
-            qCDebug(networking) << "Synchronously looking up IP address for hostname" << hostname;
+            qCDebug(networking) << "Synchronously looking up IP address for hostname" << hostname << "for" << socketType << "socket on port" << hostOrderPort;
             QHostInfo result = QHostInfo::fromName(hostname);
             handleLookupResult(result);
         } else {
-            int lookupID = QHostInfo::lookupHost(hostname, this, SLOT(handleLookupResult(QHostInfo)));
-            qCDebug(networking) << "Asynchronously looking up IP address for hostname" << hostname << "- lookup ID is" << lookupID;
+            qCDebug(networking) << "Asynchronously looking up IP address for hostname" << hostname << "for" << socketType << "socket on port" << hostOrderPort;
+            int lookupID = QHostInfo::lookupHost(hostname, this, &SockAddr::handleLookupResult);
+            qCDebug(networking) << "Lookup ID for " << hostname << "is" << lookupID;
         }
     }
 }
@@ -95,6 +96,8 @@ bool SockAddr::operator==(const SockAddr& rhsSockAddr) const {
 }
 
 void SockAddr::handleLookupResult(const QHostInfo& hostInfo) {
+    qCDebug(networking) << "handleLookupResult for" << hostInfo.lookupId();
+
     if (hostInfo.error() != QHostInfo::NoError) {
         qCDebug(networking) << "Lookup failed for" << hostInfo.lookupId() << ":" << hostInfo.errorString();
         emit lookupFailed();
diff --git a/libraries/networking/src/SocketType.h b/libraries/networking/src/SocketType.h
index 399940654d..33da7c4ea0 100644
--- a/libraries/networking/src/SocketType.h
+++ b/libraries/networking/src/SocketType.h
@@ -36,6 +36,10 @@ public:
     }
 };
 
+inline QDebug operator<<(QDebug debug, SocketType type) {
+    debug << SocketTypeToString::socketTypeToString(type);
+    return debug;
+}
 /// @}
 
 #endif // overte_SocketType_h
diff --git a/libraries/networking/src/udt/NetworkSocket.cpp b/libraries/networking/src/udt/NetworkSocket.cpp
index 179abe9f78..298455e33c 100644
--- a/libraries/networking/src/udt/NetworkSocket.cpp
+++ b/libraries/networking/src/udt/NetworkSocket.cpp
@@ -45,7 +45,7 @@ void NetworkSocket::setSocketOption(SocketType socketType, QAbstractSocket::Sock
         break;
 #endif
     default:
-        qCCritical(networking) << "Socket type not specified in setSocketOption()";
+        qCCritical(networking) << "Socket type" << socketType << "not recognized in setSocketOption()";
     }
 }
 
@@ -58,7 +58,7 @@ QVariant NetworkSocket::socketOption(SocketType socketType, QAbstractSocket::Soc
         return _webrtcSocket.socketOption(option);
 #endif
     default:
-        qCCritical(networking) << "Socket type not specified in socketOption()";
+        qCCritical(networking) << "Socket type" << socketType << "not recognized in socketOption()";
         return "";
     }
 }
@@ -75,7 +75,7 @@ void NetworkSocket::bind(SocketType socketType, const QHostAddress& address, qui
         break;
 #endif
     default:
-        qCCritical(networking) << "Socket type not specified in bind()";
+        qCCritical(networking) << "Socket type" << socketType << "not recognized in bind()";
     }
 }
 
@@ -90,7 +90,7 @@ void NetworkSocket::abort(SocketType socketType) {
         break;
 #endif
     default:
-        qCCritical(networking) << "Socket type not specified in abort()";
+        qCCritical(networking) << "Socket type" << socketType << "not recognized in abort()";
     }
 }
 
@@ -104,7 +104,7 @@ quint16 NetworkSocket::localPort(SocketType socketType) const {
         return _webrtcSocket.localPort();
 #endif
     default:
-        qCCritical(networking) << "Socket type not specified in localPort()";
+        qCCritical(networking) << "Socket type" << socketType << "not recognized in localPort()";
         return 0;
     }
 }
@@ -119,7 +119,7 @@ qintptr NetworkSocket::socketDescriptor(SocketType socketType) const {
         return 0;
 #endif
     default:
-        qCCritical(networking) << "Socket type not specified in socketDescriptor()";
+        qCCritical(networking) << "Socket type" << socketType << "not recognized in socketDescriptor()";
         return 0;
     }
 }
@@ -136,7 +136,7 @@ qint64 NetworkSocket::writeDatagram(const QByteArray& datagram, const SockAddr&
         return _webrtcSocket.writeDatagram(datagram, sockAddr);
 #endif
     default:
-        qCCritical(networking) << "Socket type not specified in writeDatagram() address";
+        qCCritical(networking) << "Socket type" << sockAddr.getType() << "not recognized in writeDatagram() address";
         return 0;
     }
 }
@@ -150,7 +150,7 @@ qint64 NetworkSocket::bytesToWrite(SocketType socketType, const SockAddr& addres
         return _webrtcSocket.bytesToWrite(address);
 #endif
     default:
-        qCCritical(networking) << "Socket type not specified in bytesToWrite()";
+        qCCritical(networking) << "Socket type" << socketType << "not recognized in bytesToWrite()";
         return 0;
     }
 }
@@ -232,7 +232,7 @@ QAbstractSocket::SocketState NetworkSocket::state(SocketType socketType) const {
         return _webrtcSocket.state();
 #endif
     default:
-        qCCritical(networking) << "Socket type not specified in state()";
+        qCCritical(networking) << "Socket type" << socketType << "not recognized in state()";
         return QAbstractSocket::SocketState::UnconnectedState;
     }
 }
@@ -247,7 +247,7 @@ QAbstractSocket::SocketError NetworkSocket::error(SocketType socketType) const {
         return _webrtcSocket.error();
 #endif
     default:
-        qCCritical(networking) << "Socket type not specified in error()";
+        qCCritical(networking) << "Socket type" << socketType << "not recognized in error()";
         return QAbstractSocket::SocketError::UnknownSocketError;
     }
 }
@@ -261,7 +261,7 @@ QString NetworkSocket::errorString(SocketType socketType) const {
         return _webrtcSocket.errorString();
 #endif
     default:
-        qCCritical(networking) << "Socket type not specified in errorString()";
+        qCCritical(networking) << "Socket type" << socketType << "not recognized in errorString()";
         return "";
     }
 }
diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp
index 4beeb5b22f..25cb1404f0 100644
--- a/libraries/physics/src/CharacterController.cpp
+++ b/libraries/physics/src/CharacterController.cpp
@@ -587,7 +587,7 @@ void CharacterController::setLocalBoundingBox(const glm::vec3& minCorner, const
     }
 
     // it's ok to change offset immediately -- there are no thread safety issues here
-    _shapeLocalOffset = minCorner + 0.5f * scale;
+    _shapeLocalOffset = glm::vec3((minCorner + 0.5f * scale).x, (minCorner + 0.5f * scale).y, -(minCorner + 0.5f * scale).z);
 
     if (_rigidBody) {
         // update CCD with new _radius
diff --git a/libraries/physics/src/CharacterController.h b/libraries/physics/src/CharacterController.h
index 8242ae4b97..90aa7ad74b 100644
--- a/libraries/physics/src/CharacterController.h
+++ b/libraries/physics/src/CharacterController.h
@@ -54,7 +54,7 @@ class CharacterController : public btCharacterControllerInterface {
 
 public:
     enum class FollowType : uint8_t {
-        Rotation,
+        Rotation = 0,
         Horizontal,
         Vertical,
         Count
diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp
index f3d129871f..ac36d37aaa 100644
--- a/libraries/physics/src/EntityMotionState.cpp
+++ b/libraries/physics/src/EntityMotionState.cpp
@@ -57,10 +57,10 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer
 
     _type = MOTIONSTATE_TYPE_ENTITY;
     assert(_entity);
-    setMass(_entity->computeMass());
     // we need the side-effects of EntityMotionState::setShape() so we call it explicitly here
     // rather than pass the legit shape pointer to the ObjectMotionState ctor above.
     setShape(shape);
+    setMass(_entity->computeMass());
 
     if (_entity->isAvatarEntity() && !_entity->isMyAvatarEntity()) {
         // avatar entities are always thus, so we cache this fact in _ownershipState
@@ -178,6 +178,11 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) {
             _body->activate();
         }
     }
+
+    if (flags & Simulation::DIRTY_MASS) {
+        setMass(_entity->computeMass());
+        updateBodyMassProperties();
+    }
 }
 
 
diff --git a/libraries/physics/src/ObjectMotionState.cpp b/libraries/physics/src/ObjectMotionState.cpp
index ad7332cb15..5376d296aa 100644
--- a/libraries/physics/src/ObjectMotionState.cpp
+++ b/libraries/physics/src/ObjectMotionState.cpp
@@ -284,10 +284,6 @@ void ObjectMotionState::handleEasyChanges(uint32_t& flags) {
     if (flags & Simulation::DIRTY_MATERIAL) {
         updateBodyMaterialProperties();
     }
-
-    if (flags & Simulation::DIRTY_MASS) {
-        updateBodyMassProperties();
-    }
 }
 
 void ObjectMotionState::updateBodyMaterialProperties() {
diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp
index c1ca853563..0281a012d2 100644
--- a/libraries/plugins/src/plugins/PluginManager.cpp
+++ b/libraries/plugins/src/plugins/PluginManager.cpp
@@ -72,12 +72,13 @@ int getPluginInterfaceVersionFromMetaData(const QJsonObject& object) {
 QStringList preferredDisplayPlugins;
 QStringList disabledDisplays;
 QStringList disabledInputs;
+std::vector<PluginManager::PluginInfo> pluginInfo;
 
 bool isDisabled(QJsonObject metaData) {
     auto name = getPluginNameFromMetaData(metaData);
     auto iid = getPluginIIDFromMetaData(metaData);
 
-    if (iid == DisplayProvider_iid) {
+    if (iid == DisplayProvider_iid || iid == SteamClientProvider_iid || iid == OculusPlatformProvider_iid) {
         return disabledDisplays.contains(name);
     } else if (iid == InputProvider_iid) {
         return disabledInputs.contains(name);
@@ -126,18 +127,28 @@ int PluginManager::instantiate() {
                 qCDebug(plugins) << "Attempting plugin" << qPrintable(plugin);
                 auto loader = QSharedPointer<QPluginLoader>::create(pluginPath + plugin);
                 const QJsonObject pluginMetaData = loader->metaData();
+
+                PluginInfo info;
+                info.name = plugin;
+                info.metaData = pluginMetaData;
+
 #if defined(HIFI_PLUGINMANAGER_DEBUG)
                 QJsonDocument metaDataDoc(pluginMetaData);
                 qCInfo(plugins) << "Metadata for " << qPrintable(plugin) << ": " << QString(metaDataDoc.toJson());
 #endif
                 if (isDisabled(pluginMetaData)) {
                     qCWarning(plugins) << "Plugin" << qPrintable(plugin) << "is disabled";
+                    info.disabled = true;
+                    pluginInfo.push_back(info);
+
                     // Skip this one, it's disabled
                     continue;
                 }
 
                 if (!_pluginFilter(pluginMetaData)) {
                     qCDebug(plugins) << "Plugin" << qPrintable(plugin) << "doesn't pass provided filter";
+                    info.filteredOut = true;
+                    pluginInfo.push_back(info);
                     continue;
                 }
 
@@ -145,16 +156,22 @@ int PluginManager::instantiate() {
                     qCWarning(plugins) << "Plugin" << qPrintable(plugin) << "interface version doesn't match, not loading:"
                                        << getPluginInterfaceVersionFromMetaData(pluginMetaData)
                                        << "doesn't match" << HIFI_PLUGIN_INTERFACE_VERSION;
+
+                    info.wrongVersion = true;
+                    pluginInfo.push_back(info);
                     continue;
                 }
 
                 if (loader->load()) {
                     qCDebug(plugins) << "Plugin" << qPrintable(plugin) << "loaded successfully";
+                    info.loaded = true;
                     loadedPlugins.push_back(loader);
                 } else {
                     qCDebug(plugins) << "Plugin" << qPrintable(plugin) << "failed to load:";
                     qCDebug(plugins) << " " << qPrintable(loader->errorString());
                 }
+
+                pluginInfo.push_back(info);
             }
         } else {
             qWarning() << "pluginPath does not exit..." << pluginDir;
@@ -163,6 +180,11 @@ int PluginManager::instantiate() {
     return loadedPlugins;
 }
 
+std::vector<PluginManager::PluginInfo> PluginManager::getPluginInfo() const {
+    getLoadedPlugins(); // This builds the pluginInfo list
+    return pluginInfo;
+}
+
 const CodecPluginList& PluginManager::getCodecPlugins() {
     static CodecPluginList codecPlugins;
     static std::once_flag once;
@@ -272,14 +294,6 @@ DisplayPluginList PluginManager::getAllDisplayPlugins() {
     return _displayPlugins;
 }
 
-void PluginManager::disableDisplayPlugin(const QString& name) {
-    auto it = std::remove_if(_displayPlugins.begin(), _displayPlugins.end(), [&](const DisplayPluginPointer& plugin){
-        return plugin->getName() == name;
-    });
-    _displayPlugins.erase(it, _displayPlugins.end());
-}
-
-
 const InputPluginList& PluginManager::getInputPlugins() {
     static std::once_flag once;
     static auto deviceAddedCallback = [&](QString deviceName) {
diff --git a/libraries/plugins/src/plugins/PluginManager.h b/libraries/plugins/src/plugins/PluginManager.h
index 26c98ce5db..b529472e1f 100644
--- a/libraries/plugins/src/plugins/PluginManager.h
+++ b/libraries/plugins/src/plugins/PluginManager.h
@@ -12,54 +12,268 @@
 
 #include <DependencyManager.h>
 #include <SettingHandle.h>
+#include <QJsonDocument>
+#include <QJsonObject>
 
 #include "Forward.h"
 
 class QPluginLoader;
 using PluginManagerPointer = QSharedPointer<PluginManager>;
 
+/**
+ * @brief Manages loadable plugins
+ *
+ * The current implementation does initialization only once, as soon as it's needed.
+ * Once things are initialized the configuration is made permanent.
+ *
+ * Both loadable and statically modules are supported. Static modules have to be provided
+ * with setDisplayPluginProvider, setInputPluginProvider and setCodecPluginProvider.
+ *
+ * @warning Users of the PluginManager must take care to do any configuration very early
+ * on, because changes become impossible once initialization is done. Plugins can't be
+ * added or removed once that happens.
+ *
+ * Initialization is performed in the getDisplayPlugins, getInputPlugins and getCodecPlugins
+ * functions.
+ */
 class PluginManager : public QObject, public Dependency {
     SINGLETON_DEPENDENCY
     Q_OBJECT
 
 public:
+
+    /**
+     * @brief Information about known plugins
+     *
+     */
+    struct PluginInfo {
+        /**
+         * @brief Plugin metadata
+        */
+        QJsonObject metaData;
+
+        /**
+         * @brief Filename
+         *
+         */
+        QString name;
+
+        /**
+         * @brief Whether the plugin has been disabled
+         *
+         */
+        bool disabled = false;
+
+        /**
+         * @brief Whether the plugin has been filtered out by a filter
+         *
+         */
+        bool filteredOut = false;
+
+        /**
+         * @brief Whether the plugin has been not loaded because it's the wrong version
+         *
+         */
+        bool wrongVersion = false;
+
+        /**
+         * @brief Whether the plugin has been loaded successfully
+         *
+         */
+        bool loaded = false;
+    };
+
+
     static PluginManagerPointer getInstance();
 
+    /**
+     * @brief Get the list of display plugins
+     *
+     * @note Calling this function will perform initialization and
+     * connects events to all the known the plugins on the first call.
+     *
+     * @return const DisplayPluginList&
+     */
     const DisplayPluginList& getDisplayPlugins();
+
+    /**
+     * @brief Get the list of input plugins
+     *
+     * @note Calling this function will perform initialization and
+     * connects events to all the known the plugins on the first call.
+     *
+     * @return const InputPluginList&
+     */
     const InputPluginList& getInputPlugins();
+
+    /**
+     * @brief Get the list of audio codec plugins
+     *
+     * @note Calling this function will perform initialization and
+     * connects events to all the known the plugins on the first call.
+     *
+     * @return const CodecPluginList&
+     */
     const CodecPluginList& getCodecPlugins();
+
+    /**
+     * @brief Get the pointer to the Steam client plugin
+     *
+     * This may return a null pointer if Steam support isn't built in.
+     *
+     * @return const SteamClientPluginPointer
+     */
     const SteamClientPluginPointer getSteamClientPlugin();
+
+    /**
+     * @brief Get the pointer to the Oculus Platform Plugin
+     *
+     * This may return a null pointer if Oculus support isn't built in.
+     *
+     * @return const OculusPlatformPluginPointer
+     */
     const OculusPlatformPluginPointer getOculusPlatformPlugin();
 
+    /**
+     * @brief Returns the list of preferred display plugins
+     *
+     * The preferred display plugins are set by setPreferredDisplayPlugins.
+     *
+     * @return DisplayPluginList
+     */
     DisplayPluginList getPreferredDisplayPlugins();
+
+    /**
+     * @brief Sets the list of preferred display plugins
+     *
+     * @note This must be called early, before any call to getPreferredDisplayPlugins.
+     *
+     * @param displays
+     */
     void setPreferredDisplayPlugins(const QStringList& displays);
 
-    void disableDisplayPlugin(const QString& name);
+    /**
+     * @brief Disable a list of displays
+     *
+     * This adds the display to a list of displays not to be used.
+     *
+     * @param displays
+     */
     void disableDisplays(const QStringList& displays);
+
+    /**
+     * @brief Disable a list of inputs
+     *
+     * This adds the input to a list of inputs not to be used.
+     * @param inputs
+     */
     void disableInputs(const QStringList& inputs);
+
+    /**
+     * @brief Save the settings
+     *
+     */
     void saveSettings();
+
+    /**
+     * @brief Set the container for plugins
+     *
+     * This will be passed to all active plugins on initialization.
+     *
+     * @param container
+     */
     void setContainer(PluginContainer* container) { _container = container; }
 
     int instantiate();
     void shutdown();
 
-    // Application that have statically linked plugins can expose them to the plugin manager with these function
+
+    /**
+     * @brief Provide a list of statically linked plugins.
+     *
+     * This is used to provide a list of statically linked plugins to the plugin manager.
+     *
+     * @note This must be called very early on, and only works once. Once the plugin manager
+     * builds its internal list of plugins, the final list becomes set in stone.
+     *
+     * @param provider A std::function that returns a list of display plugins
+     */
     void setDisplayPluginProvider(const DisplayPluginProvider& provider);
+
+    /**
+     * @brief Provide a list of statically linked plugins.
+     *
+     * This is used to provide a list of statically linked plugins to the plugin manager.
+     *
+     * @note This must be called very early on, and only works once. Once the plugin manager
+     * builds its internal list of plugins, the final list becomes set in stone.
+     *
+     * @param provider A std::function that returns a list of input plugins
+     */
     void setInputPluginProvider(const InputPluginProvider& provider);
+
+    /**
+     * @brief Provide a list of statically linked plugins.
+     *
+     * This is used to provide a list of statically linked plugins to the plugin manager.
+     *
+     * @note This must be called very early on, and only works once. Once the plugin manager
+     * builds its internal list of plugins, the final list becomes set in stone.
+     *
+     * @param provider A std::function that returns a list of codec plugins
+     */
     void setCodecPluginProvider(const CodecPluginProvider& provider);
+
+    /**
+     * @brief Set the input plugin persister
+     *
+     * @param persister A std::function that saves input plugin settings
+     */
     void setInputPluginSettingsPersister(const InputPluginSettingsPersister& persister);
+
+    /**
+     * @brief Get the list of running input devices
+     *
+     * @return QStringList List of input devices in running state
+     */
     QStringList getRunningInputDeviceNames() const;
 
     using PluginFilter = std::function<bool(const QJsonObject&)>;
+
+    /**
+     * @brief Set the plugin filter that determines whether a plugin will be used or not
+     *
+     * @note This must be called very early on. Once the plugin manager
+     * builds its internal list of plugins, the final list becomes set in stone.
+     *
+     * As of writing, this is used in the audio mixer.
+     *
+     * @param pluginFilter
+     */
     void setPluginFilter(PluginFilter pluginFilter) { _pluginFilter = pluginFilter; }
+
+    /**
+     * @brief Get a list of all the display plugins
+     *
+     * @return DisplayPluginList List of display plugins
+     */
     Q_INVOKABLE DisplayPluginList getAllDisplayPlugins();
 
     bool getEnableOculusPluginSetting() { return _enableOculusPluginSetting.get(); }
     void setEnableOculusPluginSetting(bool value);
 
+    /**
+     * @brief Returns information about known plugins
+     *
+     * This is a function for informative/debugging purposes.
+     *
+     * @return std::vector<PluginInfo>
+     */
+    std::vector<PluginInfo> getPluginInfo() const;
+
 signals:
     void inputDeviceRunningChanged(const QString& pluginName, bool isRunning, const QStringList& runningDevices);
-    
+
 private:
     PluginManager() = default;
 
diff --git a/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp b/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp
index f175a65452..f76200ea57 100644
--- a/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp
+++ b/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp
@@ -1,6 +1,7 @@
 //
 //  Created by Sam Gondelman on 2/9/2018
 //  Copyright 2018 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -128,36 +129,35 @@ NetworkMaterialResource::ParsedMaterials NetworkMaterialResource::parseMaterialF
  * @typedef {object} Entities.Material
  * @property {string} name="" - A name for the material. Supported by all material models.
  * @property {string} model="hifi_pbr" - Different material models support different properties and rendering modes.
- *     Supported models are: <code>"hifi_pbr"</code>, <code>"hifi_shader_simple"</code>.
+ *     Supported models are: <code>"hifi_pbr"</code>, <code>"hifi_shader_simple"</code>, and <code>"vrm_mtoon"</code>.
  * @property {ColorFloat|RGBS|string} emissive - The emissive color, i.e., the color that the material emits. A 
  *     {@link ColorFloat} value is treated as sRGB and must have component values in the range <code>0.0</code> &ndash; 
  *     <code>1.0</code>. A {@link RGBS} value can be either RGB or sRGB. 
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>,
+ *     <code>"vrm_mtoon"</code>.
  * @property {number|string} opacity=1.0 - The opacity, range <code>0.0</code> &ndash; <code>1.0</code>.
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> and
- *     <code>"hifi_shader_simple"</code> models only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: all.
  * @property {boolean|string} unlit=false - <code>true</code> if the material is unaffected by lighting, <code>false</code> if 
  *     it is lit by the key light and local lights.
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>.
  * @property {ColorFloat|RGBS|string} albedo - The albedo color. A {@link ColorFloat} value is treated as sRGB and must have
  *     component values in the range <code>0.0</code> &ndash; <code>1.0</code>. A {@link RGBS} value can be either RGB or sRGB.
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> and
- *     <code>"hifi_shader_simple"</code> models only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: all.
  * @property {number|string} roughness - The roughness, range <code>0.0</code> &ndash; <code>1.0</code>. 
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>.
  * @property {number|string} metallic - The metallicness, range <code>0.0</code> &ndash; <code>1.0</code>. 
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>.
  * @property {number|string} scattering - The scattering, range <code>0.0</code> &ndash; <code>1.0</code>. 
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>.
  * @property {string} emissiveMap - The URL of the emissive texture image, or an entity ID.  An entity ID may be that of an
- *     Image or Web entity.  Set to <code>"fallthrough"</code> to fall through to the material below.
- *     <code>"hifi_pbr"</code> model only.
+ *     Image or Web entity.  Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>,
+ *     <code>"vrm_mtoon"</code>.
  * @property {string} albedoMap - The URL of the albedo texture image, or an entity ID.  An entity ID may be that of an Image
- *     or Web entity.  Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code>
- *     model only.
+ *     or Web entity.  Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>,
+ *     <code>"vrm_mtoon"</code>.
  * @property {string} opacityMap - The URL of the opacity texture image, or an entity ID.  An entity ID may be that of an Image
- *     or Web entity.  Set the value the same as the <code>albedoMap</code> value for transparency.
- *     <code>"hifi_pbr"</code> model only.
+ *     or Web entity.  Set the value the same as the <code>albedoMap</code> value for transparency.  Supported models: <code>"hifi_pbr"</code>,
+ *     <code>"vrm_mtoon"</code>.
  * @property {string} opacityMapMode - The mode defining the interpretation of the opacity map. Values can be:
  *     <ul>
  *         <li><code>"OPACITY_MAP_OPAQUE"</code> for ignoring the opacity map information.</li>
@@ -166,67 +166,113 @@ NetworkMaterialResource::ParsedMaterials NetworkMaterialResource::parseMaterialF
  *         <li><code>"OPACITY_MAP_BLEND"</code> for using the <code>opacityMap</code> for alpha blending the material surface 
  *         with the background.</li>
  *     </ul>
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: all.
  * @property {number|string} opacityCutoff - The opacity cutoff threshold used to determine the opaque texels of the 
  *     <code>opacityMap</code> when <code>opacityMapMode</code> is <code>"OPACITY_MAP_MASK"</code>. Range <code>0.0</code> 
  *     &ndash; <code>1.0</code>.
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: all.
  * @property {string} cullFaceMode="CULL_BACK" - The mode defining which side of the geometry should be rendered. Values can be:
  *     <ul>
  *         <li><code>"CULL_NONE"</code> to render both sides of the geometry.</li>
  *         <li><code>"CULL_FRONT"</code> to cull the front faces of the geometry.</li>
  *         <li><code>"CULL_BACK"</code> (the default) to cull the back faces of the geometry.</li>
  *     </ul>
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
- * @property {string} cullFaceMode - The mode defining which side of the geometry should be rendered. Values can be:
- *     <ul>
- *         <li><code>"CULL_NONE"</code> for rendering both sides of the geometry.</li>
- *         <li><code>"CULL_FRONT"</code> for culling the front faces of the geometry.</li>
- *         <li><code>"CULL_BACK"</code> (the default) for culling the back faces of the geometry.</li>
- *     </ul>
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: all.
  * @property {string} roughnessMap - The URL of the roughness texture image. You can use this or <code>glossMap</code>, but not 
  *     both. 
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>.
  * @property {string} glossMap - The URL of the gloss texture image. You can use this or <code>roughnessMap</code>, but not 
  *     both. 
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>.
  * @property {string} metallicMap - The URL of the metallic texture image, or an entity ID.  An entity ID may be that of an
  *     Image or Web entity.  You can use this or <code>specularMap</code>, but not both.
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>.
  * @property {string} specularMap - The URL of the specular texture image, or an entity ID.  An entity ID may be that of an
  *     Image or Web entity.  You can use this or <code>metallicMap</code>, but not both.
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>.
  * @property {string} normalMap - The URL of the normal texture image, or an entity ID.  An entity ID may be that of an Image
  *     or Web entity.  You can use this or <code>bumpMap</code>, but not both. Set to <code>"fallthrough"</code> to fall
- *     through to the material below. <code>"hifi_pbr"</code> model only.
+ *     through to the material below. Supported models: <code>"hifi_pbr"</code>, <code>"vrm_mtoon"</code>.
  * @property {string} bumpMap - The URL of the bump texture image, or an entity ID.  An entity ID may be that of an Image
  *     or Web entity.  You can use this or <code>normalMap</code>, but not both. Set to <code>"fallthrough"</code> to
- *     fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     fall through to the material below. Supported models: <code>"hifi_pbr"</code>, <code>"vrm_mtoon"</code>.
  * @property {string} occlusionMap - The URL of the occlusion texture image, or an entity ID.  An entity ID may be that of
  *     an Image or Web entity.  Set to <code>"fallthrough"</code> to fall through to the material below.
- *     <code>"hifi_pbr"</code> model only.
+ *     Supported models: <code>"hifi_pbr"</code>.
  * @property {string} scatteringMap - The URL of the scattering texture image, or an entity ID.  An entity ID may be that of an
  *     Image or Web entity.  Only used if <code>normalMap</code> or <code>bumpMap</code> is specified.
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>.
  * @property {string} lightMap - The URL of the light map texture image, or an entity ID.  An entity ID may be that of an Image
- *     or Web entity.  Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code>
- *     model only.
+ *     or Web entity.  Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>.
  * @property {Mat4|string} texCoordTransform0 - The transform to use for all of the maps apart from <code>occlusionMap</code> 
  *     and <code>lightMap</code>. 
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>, <code>"vrm_mtoon"</code>.
  * @property {Mat4|string} texCoordTransform1 - The transform to use for <code>occlusionMap</code> and <code>lightMap</code>. 
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>, <code>"vrm_mtoon"</code>.
  * @property {string} lightmapParams - Parameters for controlling how <code>lightMap</code> is used. 
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only. 
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>. 
  *     <p><em>Currently not used.</em></p>
  * @property {string} materialParams - Parameters for controlling the material projection and repetition. 
- *     Set to <code>"fallthrough"</code> to fall through to the material below. <code>"hifi_pbr"</code> model only. 
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"hifi_pbr"</code>, <code>"vrm_mtoon"</code>.
  *     <p><em>Currently not used.</em></p>
  * @property {boolean} defaultFallthrough=false - <code>true</code> if all properties fall through to the material below 
  *     unless they are set, <code>false</code> if properties respect their individual fall-through settings. 
- *     <code>"hifi_pbr"</code> and <code>"hifi_shader_simple"</code> models only.
- * @property {ProceduralData} procedural - The definition of a procedural shader material.  <code>"hifi_shader_simple"</code> model only.
+ *     Supported models: all.
+ * @property {ProceduralData} procedural - The definition of a procedural shader material.  Supported models: <code>"hifi_shader_simple"</code>.
+ * @property {ColorFloat|RGBS|string} shade - The shade color. A {@link ColorFloat} value is treated as sRGB and must have
+ *     component values in the range <code>0.0</code> &ndash; <code>1.0</code>. A {@link RGBS} value can be either RGB or sRGB.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {string} shadeMap - The URL of the shade texture image, or an entity ID.  An entity ID may be that of an
+ *     Image or Web entity.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {number|string} shadingShift - The shading shift.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {string} shadingShiftMap - The URL of the shading shift texture image, or an entity ID.  An entity ID may be that of an
+ *     Image or Web entity.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {number|string} shadingToony - The shading toony factor. Range <code>0.0</code> &ndash; <code>1.0</code>.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {ColorFloat|RGBS|string} matcap - The matcap color. A {@link ColorFloat} value is treated as sRGB and must have
+ *     component values in the range <code>0.0</code> &ndash; <code>1.0</code>. A {@link RGBS} value can be either RGB or sRGB.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {string} matcapMap - The URL of the matcap texture image, or an entity ID.  An entity ID may be that of an
+ *     Image or Web entity.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {ColorFloat|RGBS|string} parametricRim - The rim color. A {@link ColorFloat} value is treated as sRGB and must have
+ *     component values in the range <code>0.0</code> &ndash; <code>1.0</code>. A {@link RGBS} value can be either RGB or sRGB.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {number|string} parametricRimFresnelPower - The parametric rim fresnel exponent.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {number|string} parametricRimLift - The parametric rim lift factor.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {string} rimMap - The URL of the rim texture image, or an entity ID.  An entity ID may be that of an
+ *     Image or Web entity.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {number|string} rimLightingMix - How much to mix between the rim color and normal lighting. Range <code>0.0</code>
+ *     &ndash; <code>1.0</code>.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {string} outlineWidthMode="none" - The mode defining how to render the outline. Values can be:
+ *     <ul>
+ *         <li><code>"none"</code> (the default) to not render an outline.</li>
+ *         <li><code>"worldCoordinates"</code> to render an outline with a constant world size, i.e. its apparent size depends on distance.</li>
+ *         <li><code>"screenCoordinates"</code> to render an outline with a constant screen size, i.e. its apparent size remains constant.</li>
+ *     </ul>
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {number|string} outlineWidth - The width of the outline, in meters if <code>outlineWidthMode</code> is <code>"worldCoordinates"</code>,
+ *     or a ratio of the screen height if <code>outlineWidthMode</code> is <code>"screenCoordinates"</code>.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {ColorFloat|RGBS|string} outline - The outline color. A {@link ColorFloat} value is treated as sRGB and must have
+ *     component values in the range <code>0.0</code> &ndash; <code>1.0</code>. A {@link RGBS} value can be either RGB or sRGB.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {string} uvAnimationMaskMap - The URL of the UV animation mask texture image, or an entity ID.  An entity ID may be that of an
+ *     Image or Web entity.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {number|string} uvAnimationScrollXSpeed - The speed of the UV scrolling animation in the X dimension, in UV units per second.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {number|string} uvAnimationScrollYSpeed - The speed of the UV scrolling animation in the Y dimension, in UV units per second.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
+ * @property {number|string} uvAnimationRotationSpeed - The speed of the UV scrolling rotation about (0.5, 0.5), in radians per second.
+ *     Set to <code>"fallthrough"</code> to fall through to the material below. Supported models: <code>"vrm_mtoon"</code>.
  */
 // Note: See MaterialEntityItem.h for default values used in practice.
 std::pair<std::string, std::shared_ptr<NetworkMaterial>> NetworkMaterialResource::parseJSONMaterial(const QJsonValue& materialJSONValue, const QUrl& baseUrl) {
@@ -254,8 +300,13 @@ std::pair<std::string, std::shared_ptr<NetworkMaterial>> NetworkMaterialResource
     std::array<glm::mat4, graphics::Material::NUM_TEXCOORD_TRANSFORMS> texcoordTransforms;
 
     const QString FALLTHROUGH("fallthrough");
-    if (modelString == graphics::Material::HIFI_PBR) {
-        auto material = std::make_shared<NetworkMaterial>();
+    if (modelString == graphics::Material::HIFI_PBR || modelString == graphics::Material::VRM_MTOON) {
+        std::shared_ptr<NetworkMaterial> material;
+        if (modelString == graphics::Material::HIFI_PBR) {
+            material = std::make_shared<NetworkMaterial>();
+        } else {
+            material = std::make_shared<NetworkMToonMaterial>();
+        }
         for (auto& key : materialJSON.keys()) {
             if (key == "name") {
                 auto nameJSON = materialJSON.value(key);
@@ -282,13 +333,6 @@ std::pair<std::string, std::shared_ptr<NetworkMaterial>> NetworkMaterialResource
                 } else if (value.isDouble()) {
                     material->setOpacity(value.toDouble());
                 }
-            } else if (key == "unlit") {
-                auto value = materialJSON.value(key);
-                if (value.isString() && value.toString() == FALLTHROUGH) {
-                    material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::UNLIT_VAL_BIT);
-                } else if (value.isBool()) {
-                    material->setUnlit(value.toBool());
-                }
             } else if (key == "albedo") {
                 auto value = materialJSON.value(key);
                 if (value.isString() && value.toString() == FALLTHROUGH) {
@@ -301,21 +345,7 @@ std::pair<std::string, std::shared_ptr<NetworkMaterial>> NetworkMaterialResource
                         material->setAlbedo(color, isSRGB);
                     }
                 }
-            } else if (key == "roughness") {
-                auto value = materialJSON.value(key);
-                if (value.isString() && value.toString() == FALLTHROUGH) {
-                    material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::GLOSSY_VAL_BIT);
-                } else if (value.isDouble()) {
-                    material->setRoughness(value.toDouble());
-                }
-            } else if (key == "metallic") {
-                auto value = materialJSON.value(key);
-                if (value.isString() && value.toString() == FALLTHROUGH) {
-                    material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::METALLIC_VAL_BIT);
-                } else if (value.isDouble()) {
-                    material->setMetallic(value.toDouble());
-                }
-           } else if (key == "opacityMapMode") {
+            } else if (key == "opacityMapMode") {
                 auto value = materialJSON.value(key);
                 if (value.isString()) {
                     auto valueString = value.toString();
@@ -348,14 +378,7 @@ std::pair<std::string, std::shared_ptr<NetworkMaterial>> NetworkMaterialResource
                         }
                     }
                 }
-           } else if (key == "scattering") {
-                auto value = materialJSON.value(key);
-                if (value.isString() && value.toString() == FALLTHROUGH) {
-                    material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::SCATTERING_VAL_BIT);
-                } else if (value.isDouble()) {
-                    material->setScattering(value.toDouble());
-                }
-            } else if (key == "emissiveMap") {
+           } else if (key == "emissiveMap") {
                 auto value = materialJSON.value(key);
                 if (value.isString()) {
                     auto valueString = value.toString();
@@ -380,46 +403,6 @@ std::pair<std::string, std::shared_ptr<NetworkMaterial>> NetworkMaterialResource
                         material->setAlbedoMap(baseUrl.resolved(valueString), useAlphaChannel);
                     }
                 }
-            } else if (key == "roughnessMap") {
-                auto value = materialJSON.value(key);
-                if (value.isString()) {
-                    auto valueString = value.toString();
-                    if (valueString == FALLTHROUGH) {
-                        material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::ROUGHNESS_MAP_BIT);
-                    } else {
-                        material->setRoughnessMap(baseUrl.resolved(valueString), false);
-                    }
-                }
-            } else if (key == "glossMap") {
-                auto value = materialJSON.value(key);
-                if (value.isString()) {
-                    auto valueString = value.toString();
-                    if (valueString == FALLTHROUGH) {
-                        material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::ROUGHNESS_MAP_BIT);
-                    } else {
-                        material->setRoughnessMap(baseUrl.resolved(valueString), true);
-                    }
-                }
-            } else if (key == "metallicMap") {
-                auto value = materialJSON.value(key);
-                if (value.isString()) {
-                    auto valueString = value.toString();
-                    if (valueString == FALLTHROUGH) {
-                        material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::METALLIC_MAP_BIT);
-                    } else {
-                        material->setMetallicMap(baseUrl.resolved(valueString), false);
-                    }
-                }
-            } else if (key == "specularMap") {
-                auto value = materialJSON.value(key);
-                if (value.isString()) {
-                    auto valueString = value.toString();
-                    if (valueString == FALLTHROUGH) {
-                        material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::METALLIC_MAP_BIT);
-                    } else {
-                        material->setMetallicMap(baseUrl.resolved(valueString), true);
-                    }
-                }
             } else if (key == "normalMap") {
                 auto value = materialJSON.value(key);
                 if (value.isString()) {
@@ -440,36 +423,6 @@ std::pair<std::string, std::shared_ptr<NetworkMaterial>> NetworkMaterialResource
                         material->setNormalMap(baseUrl.resolved(valueString), true);
                     }
                 }
-            } else if (key == "occlusionMap") {
-                auto value = materialJSON.value(key);
-                if (value.isString()) {
-                    auto valueString = value.toString();
-                    if (valueString == FALLTHROUGH) {
-                        material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::OCCLUSION_MAP_BIT);
-                    } else {
-                        material->setOcclusionMap(baseUrl.resolved(valueString));
-                    }
-                }
-            } else if (key == "scatteringMap") {
-                auto value = materialJSON.value(key);
-                if (value.isString()) {
-                    auto valueString = value.toString();
-                    if (valueString == FALLTHROUGH) {
-                        material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::SCATTERING_MAP_BIT);
-                    } else {
-                        material->setScatteringMap(baseUrl.resolved(valueString));
-                    }
-                }
-            } else if (key == "lightMap") {
-                auto value = materialJSON.value(key);
-                if (value.isString()) {
-                    auto valueString = value.toString();
-                    if (valueString == FALLTHROUGH) {
-                        material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::LIGHT_MAP_BIT);
-                    } else {
-                        material->setLightMap(baseUrl.resolved(valueString));
-                    }
-                }
             } else if (key == "texCoordTransform0") {
                 auto value = materialJSON.value(key);
                 if (value.isString()) {
@@ -494,15 +447,6 @@ std::pair<std::string, std::shared_ptr<NetworkMaterial>> NetworkMaterialResource
                     glm::mat4 transform = mat4FromVariant(valueVariant);
                     texcoordTransforms[1] = transform;
                 }
-            } else if (key == "lightmapParams") {
-                auto value = materialJSON.value(key);
-                if (value.isString()) {
-                    auto valueString = value.toString();
-                    if (valueString == FALLTHROUGH) {
-                        material->setPropertyDoesFallthrough(graphics::Material::ExtraFlagBit::LIGHTMAP_PARAMS);
-                    }
-                }
-                // TODO: implement lightmapParams and update JSDoc
             } else if (key == "materialParams") {
                 auto value = materialJSON.value(key);
                 if (value.isString()) {
@@ -518,6 +462,296 @@ std::pair<std::string, std::shared_ptr<NetworkMaterial>> NetworkMaterialResource
                     material->setDefaultFallthrough(value.toBool());
                 }
             }
+
+            if (modelString == graphics::Material::HIFI_PBR) {
+                if (key == "unlit") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::UNLIT_VAL_BIT);
+                    } else if (value.isBool()) {
+                        material->setUnlit(value.toBool());
+                    }
+                } else if (key == "roughness") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::GLOSSY_VAL_BIT);
+                    } else if (value.isDouble()) {
+                        material->setRoughness(value.toDouble());
+                    }
+                } else if (key == "metallic") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::METALLIC_VAL_BIT);
+                    } else if (value.isDouble()) {
+                        material->setMetallic(value.toDouble());
+                    }
+                } else if (key == "scattering") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::SCATTERING_VAL_BIT);
+                    } else if (value.isDouble()) {
+                        material->setScattering(value.toDouble());
+                    }
+                } else if (key == "roughnessMap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::ROUGHNESS_MAP_BIT);
+                        } else {
+                            material->setRoughnessMap(baseUrl.resolved(valueString), false);
+                        }
+                    }
+                } else if (key == "glossMap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::ROUGHNESS_MAP_BIT);
+                        } else {
+                            material->setRoughnessMap(baseUrl.resolved(valueString), true);
+                        }
+                    }
+                } else if (key == "metallicMap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::METALLIC_MAP_BIT);
+                        } else {
+                            material->setMetallicMap(baseUrl.resolved(valueString), false);
+                        }
+                    }
+                } else if (key == "specularMap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::METALLIC_MAP_BIT);
+                        } else {
+                            material->setMetallicMap(baseUrl.resolved(valueString), true);
+                        }
+                    }
+                } else if (key == "occlusionMap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::OCCLUSION_MAP_BIT);
+                        } else {
+                            material->setOcclusionMap(baseUrl.resolved(valueString));
+                        }
+                    }
+                } else if (key == "scatteringMap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::SCATTERING_MAP_BIT);
+                        } else {
+                            material->setScatteringMap(baseUrl.resolved(valueString));
+                        }
+                    }
+                } else if (key == "lightMap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(graphics::MaterialKey::FlagBit::LIGHT_MAP_BIT);
+                        } else {
+                            material->setLightMap(baseUrl.resolved(valueString));
+                        }
+                    }
+                } else if (key == "lightmapParams") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(graphics::Material::ExtraFlagBit::LIGHTMAP_PARAMS);
+                        }
+                    }
+                    // TODO: implement lightmapParams and update JSDoc
+                }
+            } else if (modelString == graphics::Material::VRM_MTOON) {
+                auto toonMaterial = std::static_pointer_cast<NetworkMToonMaterial>(material);
+                if (key == "shade") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::SHADE_VAL_BIT);
+                    } else {
+                        glm::vec3 color;
+                        bool isSRGB;
+                        bool valid = parseJSONColor(value, color, isSRGB);
+                        if (valid) {
+                            toonMaterial->setShade(color, isSRGB);
+                        }
+                    }
+                } else if (key == "shadeMap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::SHADE_MAP_BIT);
+                        } else {
+                            toonMaterial->setShadeMap(baseUrl.resolved(valueString));
+                        }
+                    }
+                } else if (key == "shadingShift") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::SHADING_SHIFT_VAL_BIT);
+                    } else if (value.isDouble()) {
+                        toonMaterial->setShadingShift(value.toDouble());
+                    }
+                } else if (key == "shadingShiftMap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::SHADING_SHIFT_MAP_BIT);
+                        } else {
+                            toonMaterial->setShadingShiftMap(baseUrl.resolved(valueString));
+                        }
+                    }
+                } else if (key == "shadingToony") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::SHADING_TOONY_VAL_BIT);
+                    } else if (value.isDouble()) {
+                        toonMaterial->setShadingToony(value.toDouble());
+                    }
+                } else if (key == "matcap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::MATCAP_VAL_BIT);
+                    } else {
+                        glm::vec3 color;
+                        bool isSRGB;
+                        bool valid = parseJSONColor(value, color, isSRGB);
+                        if (valid) {
+                            toonMaterial->setMatcap(color, isSRGB);
+                        }
+                    }
+                } else if (key == "matcapMap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::MATCAP_MAP_BIT);
+                        } else {
+                            toonMaterial->setMatcapMap(baseUrl.resolved(valueString));
+                        }
+                    }
+                } else if (key == "parametricRim") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::PARAMETRIC_RIM_VAL_BIT);
+                    } else {
+                        glm::vec3 color;
+                        bool isSRGB;
+                        bool valid = parseJSONColor(value, color, isSRGB);
+                        if (valid) {
+                            toonMaterial->setParametricRim(color, isSRGB);
+                        }
+                    }
+                } else if (key == "parametricRimFresnelPower") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::PARAMETRIC_RIM_POWER_VAL_BIT);
+                    } else if (value.isDouble()) {
+                        toonMaterial->setParametricRimFresnelPower(value.toDouble());
+                    }
+                } else if (key == "parametricRimLift") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::PARAMETRIC_RIM_LIFT_VAL_BIT);
+                    } else if (value.isDouble()) {
+                        toonMaterial->setParametricRimLift(value.toDouble());
+                    }
+                } else if (key == "rimMap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::RIM_MAP_BIT);
+                        } else {
+                            toonMaterial->setRimMap(baseUrl.resolved(valueString));
+                        }
+                    }
+                } else if (key == "rimLightingMix") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::RIM_LIGHTING_MIX_VAL_BIT);
+                    } else if (value.isDouble()) {
+                        toonMaterial->setRimLightingMix(value.toDouble());
+                    }
+                } else if (key == "uvAnimationMaskMap") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_MASK_MAP_BIT);
+                        } else {
+                            toonMaterial->setUVAnimationMaskMap(baseUrl.resolved(valueString));
+                        }
+                    }
+                } else if (key == "uvAnimationScrollXSpeed") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_SCROLL_VAL_BIT);
+                    } else if (value.isDouble()) {
+                        toonMaterial->setUVAnimationScrollXSpeed(value.toDouble());
+                    }
+                } else if (key == "uvAnimationScrollYSpeed") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_SCROLL_VAL_BIT);
+                    } else if (value.isDouble()) {
+                        toonMaterial->setUVAnimationScrollYSpeed(value.toDouble());
+                    }
+                } else if (key == "uvAnimationRotationSpeed") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_SCROLL_VAL_BIT);
+                    } else if (value.isDouble()) {
+                        toonMaterial->setUVAnimationRotationSpeed(value.toDouble());
+                    }
+                } else if (key == "outlineWidthMode") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString()) {
+                        auto valueString = value.toString();
+                        if (valueString == FALLTHROUGH) {
+                            material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::OUTLINE_WIDTH_MODE_VAL_BIT);
+                        } else {
+                            NetworkMToonMaterial::OutlineWidthMode mode;
+                            if (NetworkMToonMaterial::getOutlineWidthModeFromName(valueString.toStdString(), mode)) {
+                                // FIXME: Outlines are currently disabled because they're buggy
+                                //toonMaterial->setOutlineWidthMode(mode);
+                            }
+                        }
+                    }
+                } else if (key == "outlineWidth") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::OUTLINE_WIDTH_VAL_BIT);
+                    } else if (value.isDouble()) {
+                        toonMaterial->setOutlineWidth(value.toDouble());
+                    }
+                } else if (key == "outline") {
+                    auto value = materialJSON.value(key);
+                    if (value.isString() && value.toString() == FALLTHROUGH) {
+                        material->setPropertyDoesFallthrough(NetworkMToonMaterial::MToonFlagBit::OUTLINE_VAL_BIT);
+                    } else {
+                        glm::vec3 color;
+                        bool isSRGB;
+                        bool valid = parseJSONColor(value, color, isSRGB);
+                        if (valid) {
+                            toonMaterial->setOutline(color, isSRGB);
+                        }
+                    }
+                }
+                // TODO: support outlineWidthTexture and outlineLightingMix
+            }
         }
 
         // Do this after the texture maps are defined, so it overrides the default transforms
@@ -592,9 +826,9 @@ NetworkMaterial::NetworkMaterial(const NetworkMaterial& m) :
     Material(m),
     _textures(m._textures),
     _albedoTransform(m._albedoTransform),
+    _isOriginal(m._isOriginal),
     _lightmapTransform(m._lightmapTransform),
-    _lightmapParams(m._lightmapParams),
-    _isOriginal(m._isOriginal)
+    _lightmapParams(m._lightmapParams)
 {}
 
 const QString NetworkMaterial::NO_TEXTURE = QString();
@@ -631,7 +865,13 @@ graphics::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl
     }
 
     const auto url = getTextureUrl(baseUrl, hfmTexture);
-    const auto texture = DependencyManager::get<TextureCache>()->getTexture(url, type, hfmTexture.content, hfmTexture.maxNumPixels, hfmTexture.sourceChannel);
+    auto textureCache = DependencyManager::get<TextureCache>();
+    NetworkTexturePointer texture;
+    if (textureCache) {
+        texture = textureCache->getTexture(url, type, hfmTexture.content, hfmTexture.maxNumPixels, hfmTexture.sourceChannel);
+    } else {
+        qDebug() << "GeometryResource::setTextures: TextureCache dependency not available, skipping textures";
+    }
     _textures[channel] = Texture { hfmTexture.name, texture };
 
     auto map = std::make_shared<graphics::TextureMap>();
@@ -893,3 +1133,235 @@ bool NetworkMaterial::checkResetOpacityMap() {
     }
     return false;
 }
+
+NetworkMToonMaterial::NetworkMToonMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl) :
+    NetworkMaterial(material, textureBaseUrl) // handles _name, albedoMap, normalMap, and emissiveMap
+{
+    _model = VRM_MTOON;
+
+    if (!material.shadeTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.shadeTexture, image::TextureUsage::ALBEDO_TEXTURE, (MapChannel)MToonMapChannel::SHADE_MAP);
+        setTextureMap((MapChannel)MToonMapChannel::SHADE_MAP, map);
+    }
+
+    if (!material.shadingShiftTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.shadingShiftTexture, image::TextureUsage::ROUGHNESS_TEXTURE, (MapChannel)MToonMapChannel::SHADING_SHIFT_MAP);
+        setTextureMap((MapChannel)MToonMapChannel::SHADING_SHIFT_MAP, map);
+    }
+
+    if (!material.matcapTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.matcapTexture, image::TextureUsage::EMISSIVE_TEXTURE, (MapChannel)MToonMapChannel::MATCAP_MAP);
+        setTextureMap((MapChannel)MToonMapChannel::MATCAP_MAP, map);
+    }
+
+    if (!material.rimTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.rimTexture, image::TextureUsage::ALBEDO_TEXTURE, (MapChannel)MToonMapChannel::RIM_MAP);
+        setTextureMap((MapChannel)MToonMapChannel::RIM_MAP, map);
+    }
+
+    if (!material.uvAnimationTexture.filename.isEmpty()) {
+        auto map = fetchTextureMap(textureBaseUrl, material.uvAnimationTexture, image::TextureUsage::ROUGHNESS_TEXTURE, (MapChannel)MToonMapChannel::UV_ANIMATION_MASK_MAP);
+        setTextureMap((MapChannel)MToonMapChannel::UV_ANIMATION_MASK_MAP, map);
+    }
+}
+
+NetworkMToonMaterial::NetworkMToonMaterial(const NetworkMToonMaterial& material) :
+    NetworkMaterial(material),
+    _shade(material._shade),
+    _shadingShift(material._shadingShift),
+    _shadingToony(material._shadingToony),
+    _matcap(material._matcap),
+    _parametricRim(material._parametricRim),
+    _parametricRimFresnelPower(material._parametricRimFresnelPower),
+    _parametricRimLift(material._parametricRimLift),
+    _rimLightingMix(material._rimLightingMix),
+    _uvAnimationScrollXSpeed(material._uvAnimationScrollXSpeed),
+    _uvAnimationScrollYSpeed(material._uvAnimationScrollYSpeed),
+    _uvAnimationRotationSpeed(material._uvAnimationRotationSpeed),
+    _outlineWidthMode(material._outlineWidthMode),
+    _outlineWidth(material._outlineWidth),
+    _outline(material._outline)
+{}
+
+void NetworkMToonMaterial::setTextures(const QVariantMap& textureMap) {
+    _isOriginal = false;
+
+    const auto& albedoName = getTextureName(MapChannel::ALBEDO_MAP);
+    const auto& normalName = getTextureName(MapChannel::NORMAL_MAP);
+    const auto& emissiveName = getTextureName(MapChannel::EMISSIVE_MAP);
+    const auto& shadeName = getTextureName((MapChannel)MToonMapChannel::SHADE_MAP);
+    const auto& shadingShiftName = getTextureName((MapChannel)MToonMapChannel::SHADING_SHIFT_MAP);
+    const auto& matcapName = getTextureName((MapChannel)MToonMapChannel::MATCAP_MAP);
+    const auto& rimName = getTextureName((MapChannel)MToonMapChannel::RIM_MAP);
+    const auto& uvAnimationMaskName = getTextureName((MapChannel)MToonMapChannel::UV_ANIMATION_MASK_MAP);
+
+    if (!albedoName.isEmpty()) {
+        auto url = textureMap.contains(albedoName) ? textureMap[albedoName].toUrl() : QUrl();
+        auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, MapChannel::ALBEDO_MAP);
+        if (map) {
+            map->setTextureTransform(_albedoTransform);
+            // when reassigning the albedo texture we also check for the alpha channel used as opacity
+            map->setUseAlphaChannel(true);
+        }
+        setTextureMap(MapChannel::ALBEDO_MAP, map);
+    }
+
+    if (!normalName.isEmpty()) {
+        auto url = textureMap.contains(normalName) ? textureMap[normalName].toUrl() : QUrl();
+        auto map = fetchTextureMap(url, image::TextureUsage::NORMAL_TEXTURE, MapChannel::NORMAL_MAP);
+        setTextureMap(MapChannel::NORMAL_MAP, map);
+    }
+
+    if (!emissiveName.isEmpty()) {
+        auto url = textureMap.contains(emissiveName) ? textureMap[emissiveName].toUrl() : QUrl();
+        auto map = fetchTextureMap(url, image::TextureUsage::EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP);
+        setTextureMap(MapChannel::EMISSIVE_MAP, map);
+    }
+
+    if (!shadeName.isEmpty()) {
+        auto url = textureMap.contains(shadeName) ? textureMap[shadeName].toUrl() : QUrl();
+        auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, (MapChannel)MToonMapChannel::SHADE_MAP);
+        setTextureMap((MapChannel)MToonMapChannel::SHADE_MAP, map);
+    }
+
+    if (!shadingShiftName.isEmpty()) {
+        auto url = textureMap.contains(shadingShiftName) ? textureMap[shadingShiftName].toUrl() : QUrl();
+        auto map = fetchTextureMap(url, image::TextureUsage::ROUGHNESS_TEXTURE, (MapChannel)MToonMapChannel::SHADING_SHIFT_MAP);
+        setTextureMap((MapChannel)MToonMapChannel::SHADING_SHIFT_MAP, map);
+    }
+
+    if (!matcapName.isEmpty()) {
+        auto url = textureMap.contains(matcapName) ? textureMap[matcapName].toUrl() : QUrl();
+        auto map = fetchTextureMap(url, image::TextureUsage::EMISSIVE_TEXTURE, (MapChannel)MToonMapChannel::MATCAP_MAP);
+        setTextureMap((MapChannel)MToonMapChannel::MATCAP_MAP, map);
+    }
+
+    if (!rimName.isEmpty()) {
+        auto url = textureMap.contains(rimName) ? textureMap[rimName].toUrl() : QUrl();
+        auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, (MapChannel)MToonMapChannel::RIM_MAP);
+        setTextureMap((MapChannel)MToonMapChannel::RIM_MAP, map);
+    }
+
+    if (!uvAnimationMaskName.isEmpty()) {
+        auto url = textureMap.contains(uvAnimationMaskName) ? textureMap[uvAnimationMaskName].toUrl() : QUrl();
+        auto map = fetchTextureMap(url, image::TextureUsage::ROUGHNESS_TEXTURE, (MapChannel)MToonMapChannel::UV_ANIMATION_MASK_MAP);
+        setTextureMap((MapChannel)MToonMapChannel::UV_ANIMATION_MASK_MAP, map);
+    }
+}
+
+std::string NetworkMToonMaterial::getOutlineWidthModeName(OutlineWidthMode mode) {
+    const std::string names[3] = { "none", "worldCoordinates", "screenCoordinates" };
+    return names[mode];
+}
+
+bool NetworkMToonMaterial::getOutlineWidthModeFromName(const std::string& modeName, OutlineWidthMode& mode) {
+    for (int i = OUTLINE_NONE; i < NUM_OUTLINE_MODES; i++) {
+        mode = (OutlineWidthMode)i;
+        if (modeName == getOutlineWidthModeName(mode)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+void NetworkMToonMaterial::setShadeMap(const QUrl& url) {
+    auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, (MapChannel) MToonMapChannel::SHADE_MAP);
+    if (map) {
+        setTextureMap((MapChannel) MToonMapChannel::SHADE_MAP, map);
+    }
+}
+
+void NetworkMToonMaterial::setShadingShiftMap(const QUrl& url) {
+    auto map = fetchTextureMap(url, image::TextureUsage::ROUGHNESS_TEXTURE, (MapChannel) MToonMapChannel::SHADING_SHIFT_MAP);
+    if (map) {
+        setTextureMap((MapChannel) MToonMapChannel::SHADING_SHIFT_MAP, map);
+    }
+}
+
+void NetworkMToonMaterial::setMatcapMap(const QUrl& url) {
+    auto map = fetchTextureMap(url, image::TextureUsage::EMISSIVE_TEXTURE, (MapChannel)MToonMapChannel::MATCAP_MAP);
+    if (map) {
+        setTextureMap((MapChannel) MToonMapChannel::MATCAP_MAP, map);
+    }
+}
+
+void NetworkMToonMaterial::setRimMap(const QUrl& url) {
+    auto map = fetchTextureMap(url, image::TextureUsage::ALBEDO_TEXTURE, (MapChannel)MToonMapChannel::RIM_MAP);
+    if (map) {
+        setTextureMap((MapChannel) MToonMapChannel::RIM_MAP, map);
+    }
+}
+
+void NetworkMToonMaterial::setUVAnimationMaskMap(const QUrl& url) {
+    auto map = fetchTextureMap(url, image::TextureUsage::ROUGHNESS_TEXTURE, (MapChannel)MToonMapChannel::UV_ANIMATION_MASK_MAP);
+    if (map) {
+        setTextureMap((MapChannel) MToonMapChannel::UV_ANIMATION_MASK_MAP, map);
+    }
+}
+
+void NetworkMToonMaterial::setShade(const glm::vec3& shade, bool isSRGB) {
+    _key._flags.set(NetworkMToonMaterial::MToonFlagBit::SHADE_VAL_BIT, true);
+    _shade = (isSRGB ? ColorUtils::sRGBToLinearVec3(shade) : shade);
+}
+
+void NetworkMToonMaterial::setShadingShift(float shadingShift) {
+    _key._flags.set(NetworkMToonMaterial::MToonFlagBit::SHADING_SHIFT_VAL_BIT, true);
+    _shadingShift = shadingShift;
+}
+
+void NetworkMToonMaterial::setShadingToony(float shadingToony) {
+    _key._flags.set(NetworkMToonMaterial::MToonFlagBit::SHADING_TOONY_VAL_BIT, true);
+    _shadingToony = shadingToony;
+}
+
+void NetworkMToonMaterial::setMatcap(const glm::vec3& matcap, bool isSRGB) {
+    _key._flags.set(MToonFlagBit::MATCAP_VAL_BIT, true);
+    _matcap = (isSRGB ? ColorUtils::sRGBToLinearVec3(matcap) : matcap);
+}
+
+void NetworkMToonMaterial::setParametricRim(const glm::vec3& parametricRim, bool isSRGB) {
+    _key._flags.set(MToonFlagBit::PARAMETRIC_RIM_VAL_BIT, true);
+    _parametricRim = (isSRGB ? ColorUtils::sRGBToLinearVec3(parametricRim) : parametricRim);
+}
+
+void NetworkMToonMaterial::setParametricRimFresnelPower(float parametricRimFresnelPower) {
+    _key._flags.set(MToonFlagBit::PARAMETRIC_RIM_POWER_VAL_BIT, true);
+    _parametricRimFresnelPower = parametricRimFresnelPower;
+}
+
+void NetworkMToonMaterial::setParametricRimLift(float parametricRimLift) {
+    _key._flags.set(MToonFlagBit::PARAMETRIC_RIM_LIFT_VAL_BIT, true);
+    _parametricRimLift = parametricRimLift;
+}
+
+void NetworkMToonMaterial::setRimLightingMix(float rimLightingMix) {
+    _key._flags.set(MToonFlagBit::RIM_LIGHTING_MIX_VAL_BIT, true);
+    _rimLightingMix = rimLightingMix;
+}
+
+void NetworkMToonMaterial::setUVAnimationScrollXSpeed(float uvAnimationScrollXSpeed) {
+    _key._flags.set(NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_SCROLL_VAL_BIT, true);
+    _uvAnimationScrollXSpeed = uvAnimationScrollXSpeed;
+}
+
+void NetworkMToonMaterial::setUVAnimationScrollYSpeed(float uvAnimationScrollYSpeed) {
+    _key._flags.set(NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_SCROLL_VAL_BIT, true);
+    _uvAnimationScrollYSpeed = uvAnimationScrollYSpeed;
+}
+
+void NetworkMToonMaterial::setUVAnimationRotationSpeed(float uvAnimationRotationSpeed) {
+    _key._flags.set(NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_SCROLL_VAL_BIT, true);
+    _uvAnimationRotationSpeed = uvAnimationRotationSpeed;
+}
+
+void NetworkMToonMaterial::setOutlineWidthMode(OutlineWidthMode mode) {
+    _outlineWidthMode = mode;
+}
+
+void NetworkMToonMaterial::setOutlineWidth(float width) {
+    _outlineWidth = width;
+}
+
+void NetworkMToonMaterial::setOutline(const glm::vec3& outline, bool isSRGB) {
+    _outline = (isSRGB ? ColorUtils::sRGBToLinearVec3(outline) : outline);
+}
diff --git a/libraries/procedural/src/procedural/ProceduralMaterialCache.h b/libraries/procedural/src/procedural/ProceduralMaterialCache.h
index 7d6a6ecdf3..a7c5baa011 100644
--- a/libraries/procedural/src/procedural/ProceduralMaterialCache.h
+++ b/libraries/procedural/src/procedural/ProceduralMaterialCache.h
@@ -1,6 +1,7 @@
 //
 //  Created by Sam Gondelman on 2/9/2018
 //  Copyright 2018 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -59,22 +60,141 @@ protected:
     static const QString NO_TEXTURE;
     const QString& getTextureName(MapChannel channel);
 
-    void setTextures(const QVariantMap& textureMap);
+    virtual void setTextures(const QVariantMap& textureMap);
 
     const bool& isOriginal() const { return _isOriginal; }
 
-private:
-    // Helpers for the ctors
-    QUrl getTextureUrl(const QUrl& baseUrl, const HFMTexture& hfmTexture);
     graphics::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const HFMTexture& hfmTexture,
                                                 image::TextureUsage::Type type, MapChannel channel);
     graphics::TextureMapPointer fetchTextureMap(const QUrl& url, image::TextureUsage::Type type, MapChannel channel);
 
     Transform _albedoTransform;
-    Transform _lightmapTransform;
-    vec2 _lightmapParams;
 
     bool _isOriginal { true };
+
+private:
+    // Helpers for the ctors
+    QUrl getTextureUrl(const QUrl& baseUrl, const HFMTexture& hfmTexture);
+
+    Transform _lightmapTransform;
+    vec2 _lightmapParams;
+};
+
+class NetworkMToonMaterial : public NetworkMaterial {
+public:
+    NetworkMToonMaterial() : NetworkMaterial() { _model = VRM_MTOON; }
+    NetworkMToonMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl);
+    NetworkMToonMaterial(const NetworkMToonMaterial& material);
+
+    void setTextures(const QVariantMap& textureMap) override;
+
+    enum MToonMapChannel {
+        // Keep aligned with graphics/ShaderConstants.h and graphics-scripting/ScriptableModel.cpp
+        SHADE_MAP = MapChannel::ROUGHNESS_MAP,
+        SHADING_SHIFT_MAP = MapChannel::METALLIC_MAP,
+        MATCAP_MAP = MapChannel::OCCLUSION_MAP,
+        RIM_MAP = MapChannel::SCATTERING_MAP,
+        UV_ANIMATION_MASK_MAP = MapChannel::LIGHT_MAP,
+    };
+
+    enum MToonFlagBit {
+        // Must match mappings in GraphicsScriptingInterface.cpp
+        SHADE_MAP_BIT = graphics::MaterialKey::FlagBit::ROUGHNESS_MAP_BIT,
+        SHADING_SHIFT_MAP_BIT = graphics::MaterialKey::FlagBit::METALLIC_MAP_BIT,
+        MATCAP_MAP_BIT = graphics::MaterialKey::FlagBit::OCCLUSION_MAP_BIT,
+        RIM_MAP_BIT = graphics::MaterialKey::FlagBit::SCATTERING_MAP_BIT,
+        UV_ANIMATION_MASK_MAP_BIT = graphics::MaterialKey::FlagBit::LIGHT_MAP_BIT,
+
+        SHADE_VAL_BIT = graphics::MaterialKey::FlagBit::UNLIT_VAL_BIT,
+        SHADING_SHIFT_VAL_BIT = graphics::MaterialKey::FlagBit::METALLIC_VAL_BIT,
+        SHADING_TOONY_VAL_BIT = graphics::MaterialKey::FlagBit::GLOSSY_VAL_BIT,
+        UV_ANIMATION_SCROLL_VAL_BIT = graphics::MaterialKey::FlagBit::SCATTERING_VAL_BIT,
+        MATCAP_VAL_BIT = graphics::MaterialKey::FlagBit::EXTRA_1_BIT,
+        PARAMETRIC_RIM_VAL_BIT = graphics::MaterialKey::FlagBit::EXTRA_2_BIT,
+        PARAMETRIC_RIM_POWER_VAL_BIT = graphics::MaterialKey::FlagBit::EXTRA_3_BIT,
+        PARAMETRIC_RIM_LIFT_VAL_BIT = graphics::MaterialKey::FlagBit::EXTRA_4_BIT,
+        RIM_LIGHTING_MIX_VAL_BIT = graphics::MaterialKey::FlagBit::EXTRA_5_BIT,
+
+        OUTLINE_WIDTH_MODE_VAL_BIT = graphics::Material::ExtraFlagBit::EXTRA_1_BIT,
+        OUTLINE_WIDTH_VAL_BIT = graphics::Material::ExtraFlagBit::EXTRA_2_BIT,
+        OUTLINE_VAL_BIT = graphics::Material::ExtraFlagBit::EXTRA_3_BIT,
+    };
+
+    enum OutlineWidthMode {
+        OUTLINE_NONE = 0,
+        OUTLINE_WORLD,
+        OUTLINE_SCREEN,
+
+        NUM_OUTLINE_MODES
+    };
+    static std::string getOutlineWidthModeName(OutlineWidthMode mode);
+    // find the enum value from a string, return true if match found
+    static bool getOutlineWidthModeFromName(const std::string& modeName, OutlineWidthMode& mode);
+
+    bool isMToon() const override { return true; }
+
+    void setShadeMap(const QUrl& url);
+    void setShadingShiftMap(const QUrl& url);
+    void setMatcapMap(const QUrl& url);
+    void setRimMap(const QUrl& url);
+    void setUVAnimationMaskMap(const QUrl& url);
+
+    void setShade(const glm::vec3& shade, bool isSRGB = true);
+    glm::vec3 getShade(bool SRGB = true) const override { return (SRGB ? ColorUtils::tosRGBVec3(_shade) : _shade); }
+
+    void setShadingShift(float shadeShift);
+    float getShadingShift() const override { return _shadingShift; }
+
+    void setShadingToony(float shadingToony);
+    float getShadingToony() const override { return _shadingToony; }
+
+    void setMatcap(const glm::vec3& matcap, bool isSRGB = true);
+    glm::vec3 getMatcap(bool SRGB = true) const override { return (SRGB ? ColorUtils::tosRGBVec3(_matcap) : _matcap); }
+
+    void setParametricRim(const glm::vec3& parametricRim, bool isSRGB = true);
+    glm::vec3 getParametricRim(bool SRGB = true) const override { return (SRGB ? ColorUtils::tosRGBVec3(_parametricRim) : _parametricRim); }
+
+    void setParametricRimFresnelPower(float parametricRimFresnelPower);
+    float getParametricRimFresnelPower() const override { return _parametricRimFresnelPower; }
+
+    void setParametricRimLift(float parametricRimLift);
+    float getParametricRimLift() const override { return _parametricRimLift; }
+
+    void setRimLightingMix(float rimLightingMix);
+    float getRimLightingMix() const override { return _rimLightingMix; }
+
+    void setUVAnimationScrollXSpeed(float uvAnimationScrollXSpeed);
+    float getUVAnimationScrollXSpeed() const override { return _uvAnimationScrollXSpeed; }
+    void setUVAnimationScrollYSpeed(float uvAnimationScrollYSpeed);
+    float getUVAnimationScrollYSpeed() const override { return _uvAnimationScrollYSpeed; }
+    void setUVAnimationRotationSpeed(float uvAnimationRotationSpeed);
+    float getUVAnimationRotationSpeed() const override { return _uvAnimationRotationSpeed; }
+
+    void setOutlineWidthMode(OutlineWidthMode mode);
+    uint8_t getOutlineWidthMode() override { return _outlineWidthMode; }
+    void setOutlineWidth(float width);
+    float getOutlineWidth() override { return _outlineWidth; }
+    void setOutline(const glm::vec3& outline, bool isSRGB = true);
+    glm::vec3 getOutline(bool SRGB = true) const override { return (SRGB ? ColorUtils::tosRGBVec3(_outline) : _outline); }
+
+private:
+    glm::vec3 _shade { DEFAULT_SHADE };
+    float _shadingShift { DEFAULT_SHADING_SHIFT };
+    float _shadingToony { DEFAULT_SHADING_TOONY };
+
+    glm::vec3 _matcap { DEFAULT_MATCAP };
+    glm::vec3 _parametricRim { DEFAULT_PARAMETRIC_RIM };
+    float _parametricRimFresnelPower { DEFAULT_PARAMETRIC_RIM_FRESNEL_POWER };
+    float _parametricRimLift { DEFAULT_PARAMETRIC_RIM_LIFT };
+    float _rimLightingMix { DEFAULT_RIM_LIGHTING_MIX };
+
+    float _uvAnimationScrollXSpeed { DEFAULT_UV_ANIMATION_SCROLL_SPEED };
+    float _uvAnimationScrollYSpeed { DEFAULT_UV_ANIMATION_SCROLL_SPEED };
+    float _uvAnimationRotationSpeed { DEFAULT_UV_ANIMATION_SCROLL_SPEED };
+
+    OutlineWidthMode _outlineWidthMode { OutlineWidthMode::OUTLINE_NONE };
+    float _outlineWidth { 0.0f };
+    glm::vec3 _outline { DEFAULT_OUTLINE };
 };
 
 class NetworkMaterialResource : public Resource {
diff --git a/libraries/procedural/src/procedural/ReferenceMaterial.cpp b/libraries/procedural/src/procedural/ReferenceMaterial.cpp
index 97211eb737..65bea1bf7a 100644
--- a/libraries/procedural/src/procedural/ReferenceMaterial.cpp
+++ b/libraries/procedural/src/procedural/ReferenceMaterial.cpp
@@ -1,6 +1,7 @@
 //
 //  Created by HifiExperiments on 3/14/2021
 //  Copyright 2021 Vircadia contributors.
+//  Copyright 2024 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
@@ -162,7 +163,7 @@ bool ReferenceMaterial::isReady() const {
 
 QString ReferenceMaterial::getProceduralString() const {
     return resultWithLock<QString>([&] {
-        auto material = getMaterial();
+        auto material = getProceduralMaterial();
         return material ? material->getProceduralString() : QString();
     });
 }
@@ -212,6 +213,112 @@ void ReferenceMaterial::initializeProcedural() {
     });
 }
 
+// MToonMaterial
+bool ReferenceMaterial::isMToon() const {
+    return resultWithLock<bool>([&] {
+        auto material = getMaterial();
+        return material ? material->isMToon() : false;
+    });
+}
+
+glm::vec3 ReferenceMaterial::getShade(bool SRGB) const {
+    return resultWithLock<glm::vec3>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getShade(SRGB) : glm::vec3();
+    });
+}
+
+float ReferenceMaterial::getShadingShift() const {
+    return resultWithLock<float>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getShadingShift() : 0.0f;
+    });
+}
+
+float ReferenceMaterial::getShadingToony() const {
+    return resultWithLock<float>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getShadingToony() : 0.0f;
+    });
+}
+
+glm::vec3 ReferenceMaterial::getMatcap(bool SRGB) const {
+    return resultWithLock<glm::vec3>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getMatcap(SRGB) : glm::vec3();
+    });
+}
+
+glm::vec3 ReferenceMaterial::getParametricRim(bool SRGB) const {
+    return resultWithLock<glm::vec3>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getParametricRim(SRGB) : glm::vec3();
+    });
+}
+
+float ReferenceMaterial::getParametricRimFresnelPower() const {
+    return resultWithLock<float>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getParametricRimFresnelPower() : 0.0f;
+    });
+}
+
+float ReferenceMaterial::getParametricRimLift() const {
+    return resultWithLock<float>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getParametricRimLift() : 0.0f;
+    });
+}
+
+float ReferenceMaterial::getRimLightingMix() const {
+    return resultWithLock<float>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getRimLightingMix() : 0.0f;
+    });
+}
+
+float ReferenceMaterial::getUVAnimationScrollXSpeed() const {
+    return resultWithLock<float>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getUVAnimationScrollXSpeed() : 0.0f;
+    });
+}
+
+float ReferenceMaterial::getUVAnimationScrollYSpeed() const {
+    return resultWithLock<float>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getUVAnimationScrollYSpeed() : 0.0f;
+    });
+}
+
+float ReferenceMaterial::getUVAnimationRotationSpeed() const {
+    return resultWithLock<float>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getUVAnimationRotationSpeed() : 0.0f;
+    });
+}
+
+uint8_t ReferenceMaterial::getOutlineWidthMode() {
+    return resultWithLock<uint8_t>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getOutlineWidthMode() : 0;
+    });
+}
+
+float ReferenceMaterial::getOutlineWidth() {
+    return resultWithLock<float>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getOutlineWidth() : 0.0f;
+    });
+}
+
+glm::vec3 ReferenceMaterial::getOutline(bool SRGB) const {
+    return resultWithLock<glm::vec3>([&] {
+        auto material = getMToonMaterial();
+        return material ? material->getOutline() : glm::vec3(0.0f);
+    });
+}
+
 void ReferenceMaterial::setMaterialForUUIDOperator(std::function<graphics::MaterialPointer(QUuid)> materialForUUIDOperator) {
     _unboundMaterialForUUIDOperator = materialForUUIDOperator;
 }
@@ -244,6 +351,16 @@ graphics::ProceduralMaterialPointer ReferenceMaterial::getProceduralMaterial() c
     return nullptr;
 }
 
+std::shared_ptr<NetworkMToonMaterial> ReferenceMaterial::getMToonMaterial() const {
+    if (_materialForUUIDOperator) {
+        std::shared_ptr<NetworkMToonMaterial> result = nullptr;
+        if (auto material = _materialForUUIDOperator()) {
+            return std::static_pointer_cast<NetworkMToonMaterial>(material);
+        }
+    }
+    return nullptr;
+}
+
 template <typename T, typename F>
 inline T ReferenceMaterial::resultWithLock(F&& f) const {
     if (_locked) {
diff --git a/libraries/procedural/src/procedural/ReferenceMaterial.h b/libraries/procedural/src/procedural/ReferenceMaterial.h
index ac778f94b1..140b86fe33 100644
--- a/libraries/procedural/src/procedural/ReferenceMaterial.h
+++ b/libraries/procedural/src/procedural/ReferenceMaterial.h
@@ -1,6 +1,7 @@
 //
 //  Created by HifiExperiments on 3/14/2021
 //  Copyright 2021 Vircadia contributors.
+//  Copyright 2024 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
@@ -52,6 +53,23 @@ public:
                  const uint64_t& created, const ProceduralProgramKey key = ProceduralProgramKey()) override;
     void initializeProcedural() override;
 
+    // MToonMaterial
+    bool isMToon() const override;
+    glm::vec3 getShade(bool SRGB = true) const override;
+    float getShadingShift() const override;
+    float getShadingToony() const override;
+    glm::vec3 getMatcap(bool SRGB = true) const override;
+    glm::vec3 getParametricRim(bool SRGB = true) const override;
+    float getParametricRimFresnelPower() const override;
+    float getParametricRimLift() const override;
+    float getRimLightingMix() const override;
+    float getUVAnimationScrollXSpeed() const override;
+    float getUVAnimationScrollYSpeed() const override;
+    float getUVAnimationRotationSpeed() const override;
+    uint8_t getOutlineWidthMode() override;
+    float getOutlineWidth() override;
+    glm::vec3 getOutline(bool SRGB = true) const override;
+
     bool isReference() const override { return true; }
     std::function<graphics::MaterialPointer()> getReferenceOperator() const { return _materialForUUIDOperator; }
 
@@ -65,6 +83,7 @@ private:
     graphics::MaterialPointer getMaterial() const;
     std::shared_ptr<NetworkMaterial> getNetworkMaterial() const;
     graphics::ProceduralMaterialPointer getProceduralMaterial() const;
+    std::shared_ptr<NetworkMToonMaterial> getMToonMaterial() const;
 
     template <typename T, typename F>
     T resultWithLock(F&& f) const;
diff --git a/libraries/recording/src/recording/RecordingScriptingInterface.cpp b/libraries/recording/src/recording/RecordingScriptingInterface.cpp
index 05cfa8b851..a05ee60604 100644
--- a/libraries/recording/src/recording/RecordingScriptingInterface.cpp
+++ b/libraries/recording/src/recording/RecordingScriptingInterface.cpp
@@ -38,28 +38,36 @@ using namespace recording;
 static const QString HFR_EXTENSION = "hfr";
 
 RecordingScriptingInterface::RecordingScriptingInterface() {
+    Locker lock(_mutex);
     _player = DependencyManager::get<Deck>();
     _recorder = DependencyManager::get<Recorder>();
 }
 
 bool RecordingScriptingInterface::isPlaying() const {
+    Locker lock(_mutex);
     return _player->isPlaying();
 }
 
 bool RecordingScriptingInterface::isPaused() const {
+    Locker lock(_mutex);
     return _player->isPaused();
 }
 
 float RecordingScriptingInterface::playerElapsed() const {
+    Locker lock(_mutex);
     return _player->position();
 }
 
 float RecordingScriptingInterface::playerLength() const {
+    Locker lock(_mutex);
     return _player->length();
 }
 
 void RecordingScriptingInterface::playClip(NetworkClipLoaderPointer clipLoader, const QString& url, const ScriptValue& callback) {
-    _player->queueClip(clipLoader->getClip());
+    {
+        Locker lock(_mutex);
+        _player->queueClip(clipLoader->getClip());
+    }
 
     if (callback.isFunction()) {
         auto engine = callback.engine();
@@ -69,11 +77,6 @@ void RecordingScriptingInterface::playClip(NetworkClipLoaderPointer clipLoader,
 }
 
 void RecordingScriptingInterface::loadRecording(const QString& url, const ScriptValue& callback) {
-    if (QThread::currentThread() != thread()) {
-        BLOCKING_INVOKE_METHOD(this, "loadRecording", Q_ARG(const QString&, url), Q_ARG(const ScriptValue&, callback));
-        return;
-    }
-
     auto clipLoader = DependencyManager::get<recording::ClipCache>()->getClipLoader(url);
 
     if (clipLoader->isLoaded()) {
@@ -82,6 +85,8 @@ void RecordingScriptingInterface::loadRecording(const QString& url, const Script
         return;
     }
 
+    Locker lock(_mutex);
+
     // hold a strong pointer to the loading clip so that it has a chance to load
     _clipLoaders.insert(clipLoader);
 
@@ -131,15 +136,12 @@ void RecordingScriptingInterface::startPlaying() {
         return;
     }
 
+    Locker lock(_mutex);
     _player->play();
 }
 
 void RecordingScriptingInterface::setPlayerVolume(float volume) {
-    if (QThread::currentThread() != thread()) {
-        BLOCKING_INVOKE_METHOD(this, "setPlayerVolume", Q_ARG(float, volume));
-        return;
-    }
-
+    Locker lock(_mutex);
     _player->setVolume(std::min(std::max(volume, 0.0f), 1.0f));
 }
 
@@ -152,6 +154,8 @@ void RecordingScriptingInterface::setPlayerTime(float time) {
         BLOCKING_INVOKE_METHOD(this, "setPlayerTime", Q_ARG(float, time));
         return;
     }
+
+    Locker lock(_mutex);
     _player->seek(time);
 }
 
@@ -160,11 +164,7 @@ void RecordingScriptingInterface::setPlayFromCurrentLocation(bool playFromCurren
 }
 
 void RecordingScriptingInterface::setPlayerLoop(bool loop) {
-    if (QThread::currentThread() != thread()) {
-        BLOCKING_INVOKE_METHOD(this, "setPlayerLoop", Q_ARG(bool, loop));
-        return;
-    }
-
+    Locker lock(_mutex);
     _player->loop(loop);
 }
 
@@ -185,10 +185,7 @@ void RecordingScriptingInterface::setPlayerUseSkeletonModel(bool useSkeletonMode
 }
 
 void RecordingScriptingInterface::pausePlayer() {
-    if (QThread::currentThread() != thread()) {
-        BLOCKING_INVOKE_METHOD(this, "pausePlayer");
-        return;
-    }
+    Locker lock(_mutex);
     _player->pause();
 }
 
@@ -197,6 +194,8 @@ void RecordingScriptingInterface::stopPlaying() {
         BLOCKING_INVOKE_METHOD(this, "stopPlaying");
         return;
     }
+
+    Locker lock(_mutex);
     _player->stop();
 }
 
@@ -214,11 +213,7 @@ void RecordingScriptingInterface::startRecording() {
         return;
     }
 
-    if (QThread::currentThread() != thread()) {
-        BLOCKING_INVOKE_METHOD(this, "startRecording");
-        return;
-    }
-
+    Locker lock(_mutex);
     _recorder->start();
 }
 
@@ -228,11 +223,7 @@ void RecordingScriptingInterface::stopRecording() {
         return;
     }
 
-    if (QThread::currentThread() != thread()) {
-        BLOCKING_INVOKE_METHOD(this, "stopRecording");
-        return;
-    }
-
+    Locker lock(_mutex);
     _recorder->stop();
     _lastClip = _recorder->getClip();
     _lastClip->seek(0);
@@ -247,12 +238,7 @@ QString RecordingScriptingInterface::getDefaultRecordingSaveDirectory() {
 }
 
 void RecordingScriptingInterface::saveRecording(const QString& filename) {
-    if (QThread::currentThread() != thread()) {
-        BLOCKING_INVOKE_METHOD(this, "saveRecording",
-            Q_ARG(QString, filename));
-        return;
-    }
-
+    Locker lock(_mutex);
     if (!_lastClip) {
         qWarning() << "There is no recording to save";
         return;
@@ -267,14 +253,7 @@ bool RecordingScriptingInterface::saveRecordingToAsset(const ScriptValue& getCli
         return false;
     }
 
-    if (QThread::currentThread() != thread()) {
-        bool result;
-        BLOCKING_INVOKE_METHOD(this, "saveRecordingToAsset",
-            Q_RETURN_ARG(bool, result),
-            Q_ARG(const ScriptValue&, getClipAtpUrl));
-        return result;
-    }
-
+    Locker lock(_mutex);
     if (!_lastClip) {
         qWarning() << "There is no recording to save";
         return false;
@@ -316,6 +295,8 @@ void RecordingScriptingInterface::loadLastRecording() {
         return;
     }
 
+    Locker lock(_mutex);
+
     if (!_lastClip) {
         qCDebug(scriptengine) << "There is no recording to load";
         return;
diff --git a/libraries/recording/src/recording/RecordingScriptingInterface.h b/libraries/recording/src/recording/RecordingScriptingInterface.h
index 42dc665706..394c3e230d 100644
--- a/libraries/recording/src/recording/RecordingScriptingInterface.h
+++ b/libraries/recording/src/recording/RecordingScriptingInterface.h
@@ -357,6 +357,8 @@ protected:
     using Locker = std::unique_lock<Mutex>;
     using Flag = std::atomic<bool>;
 
+    mutable Mutex _mutex;
+
     QSharedPointer<recording::Deck> _player;
     QSharedPointer<recording::Recorder> _recorder;
     
diff --git a/libraries/render-utils/res/fonts/CourierPrime-OFL.txt b/libraries/render-utils/res/fonts/CourierPrime-OFL.txt
new file mode 100644
index 0000000000..28f87c184d
--- /dev/null
+++ b/libraries/render-utils/res/fonts/CourierPrime-OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2015 The Courier Prime Project Authors (https://github.com/quoteunquoteapps/CourierPrime).
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded, 
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/libraries/render-utils/res/fonts/CourierPrime.arfont b/libraries/render-utils/res/fonts/CourierPrime.arfont
new file mode 100644
index 0000000000..3396ccfb86
Binary files /dev/null and b/libraries/render-utils/res/fonts/CourierPrime.arfont differ
diff --git a/libraries/render-utils/res/fonts/CourierPrime.license b/libraries/render-utils/res/fonts/CourierPrime.license
deleted file mode 100644
index 661d34a312..0000000000
--- a/libraries/render-utils/res/fonts/CourierPrime.license
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
-
-SIL Open Font License v1.10
-
-This license can also be found at this permalink: http://www.fontsquirrel.com/license/courier-prime
-
-Copyright (c) 2013, Quote-Unquote Apps (http://quoteunquoteapps.com),
-with Reserved Font Name Courier Prime.
-
-This Font Software is licensed under the SIL Open Font License, Version 1.1.
-This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-
-—————————————————————————————-
-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-—————————————————————————————-
-
-PREAMBLE
-The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
-
-The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
-
-DEFINITIONS
-“Font Software” refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
-
-“Reserved Font Name” refers to any names specified as such after the copyright statement(s).
-
-“Original Version” refers to the collection of Font Software components as distributed by the Copyright Holder(s).
-
-“Modified Version” refers to any derivative made by adding to, deleting, or substituting—in part or in whole—any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
-
-“Author” refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
-
-PERMISSION & CONDITIONS
-Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
-
-1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
-
-2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
-
-3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
-
-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
-
-5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
-
-TERMINATION
-This license becomes null and void if any of the above conditions are not met.
-
-DISCLAIMER
-THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
-*/
\ No newline at end of file
diff --git a/libraries/render-utils/res/fonts/CourierPrime.sdff b/libraries/render-utils/res/fonts/CourierPrime.sdff
deleted file mode 100644
index f8e6687323..0000000000
Binary files a/libraries/render-utils/res/fonts/CourierPrime.sdff and /dev/null differ
diff --git a/libraries/render-utils/res/fonts/Inconsolata-OFL.txt b/libraries/render-utils/res/fonts/Inconsolata-OFL.txt
new file mode 100644
index 0000000000..1089cbf838
--- /dev/null
+++ b/libraries/render-utils/res/fonts/Inconsolata-OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2006 The Inconsolata Project Authors
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded, 
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/libraries/render-utils/res/fonts/InconsolataMedium.arfont b/libraries/render-utils/res/fonts/InconsolataMedium.arfont
new file mode 100644
index 0000000000..0b0e3750fe
Binary files /dev/null and b/libraries/render-utils/res/fonts/InconsolataMedium.arfont differ
diff --git a/libraries/render-utils/res/fonts/InconsolataMedium.sdff b/libraries/render-utils/res/fonts/InconsolataMedium.sdff
deleted file mode 100644
index d66e25b34b..0000000000
Binary files a/libraries/render-utils/res/fonts/InconsolataMedium.sdff and /dev/null differ
diff --git a/libraries/render-utils/res/fonts/Roboto-LICENSE.txt b/libraries/render-utils/res/fonts/Roboto-LICENSE.txt
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/libraries/render-utils/res/fonts/Roboto-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/libraries/render-utils/res/fonts/Roboto.arfont b/libraries/render-utils/res/fonts/Roboto.arfont
new file mode 100644
index 0000000000..b93b738e1a
Binary files /dev/null and b/libraries/render-utils/res/fonts/Roboto.arfont differ
diff --git a/libraries/render-utils/res/fonts/Roboto.sdff b/libraries/render-utils/res/fonts/Roboto.sdff
deleted file mode 100644
index e0ff17a637..0000000000
Binary files a/libraries/render-utils/res/fonts/Roboto.sdff and /dev/null differ
diff --git a/libraries/render-utils/res/fonts/Timeless.arfont b/libraries/render-utils/res/fonts/Timeless.arfont
new file mode 100644
index 0000000000..6d2674deeb
Binary files /dev/null and b/libraries/render-utils/res/fonts/Timeless.arfont differ
diff --git a/libraries/render-utils/res/fonts/Timeless.sdff b/libraries/render-utils/res/fonts/Timeless.sdff
deleted file mode 100644
index be62f42de3..0000000000
Binary files a/libraries/render-utils/res/fonts/Timeless.sdff and /dev/null differ
diff --git a/libraries/render-utils/res/fonts/fonts.qrc b/libraries/render-utils/res/fonts/fonts.qrc
index a7d9e6c3df..2c31697d5e 100644
--- a/libraries/render-utils/res/fonts/fonts.qrc
+++ b/libraries/render-utils/res/fonts/fonts.qrc
@@ -1,9 +1,9 @@
 <!DOCTYPE RCC>
 <RCC version="1.0">
   <qresource>
-      <file>CourierPrime.sdff</file>
-      <file>InconsolataMedium.sdff</file>
-      <file>Roboto.sdff</file>
-      <file>Timeless.sdff</file>
+      <file>CourierPrime.arfont</file>
+      <file>InconsolataMedium.arfont</file>
+      <file>Roboto.arfont</file>
+      <file>Timeless.arfont</file>
   </qresource>
 </RCC>
\ No newline at end of file
diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp
index 4ac097e31d..4d4f9c0680 100644
--- a/libraries/render-utils/src/GeometryCache.cpp
+++ b/libraries/render-utils/src/GeometryCache.cpp
@@ -99,14 +99,9 @@ static const gpu::Element NORMAL_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ };
 static const gpu::Element TEXCOORD0_ELEMENT { gpu::VEC2, gpu::FLOAT, gpu::UV };
 static const gpu::Element TANGENT_ELEMENT { gpu::VEC3, gpu::FLOAT, gpu::XYZ };
 static const gpu::Element COLOR_ELEMENT { gpu::VEC4, gpu::NUINT8, gpu::RGBA };
-static const gpu::Element TEXCOORD4_ELEMENT { gpu::VEC4, gpu::FLOAT, gpu::XYZW };
 
 static gpu::Stream::FormatPointer SOLID_STREAM_FORMAT;
-static gpu::Stream::FormatPointer INSTANCED_SOLID_STREAM_FORMAT;
-static gpu::Stream::FormatPointer INSTANCED_SOLID_FADE_STREAM_FORMAT;
 static gpu::Stream::FormatPointer WIRE_STREAM_FORMAT;
-static gpu::Stream::FormatPointer INSTANCED_WIRE_STREAM_FORMAT;
-static gpu::Stream::FormatPointer INSTANCED_WIRE_FADE_STREAM_FORMAT;
 
 static const uint SHAPE_VERTEX_STRIDE = sizeof(GeometryCache::ShapeVertex); // position, normal, texcoords, tangent
 static const uint SHAPE_NORMALS_OFFSET = offsetof(GeometryCache::ShapeVertex, normal);
@@ -259,14 +254,6 @@ size_t GeometryCache::getShapeTriangleCount(Shape shape) {
     return _shapes[shape]._indicesView.getNumElements() / VERTICES_PER_TRIANGLE;
 }
 
-size_t GeometryCache::getSphereTriangleCount() {
-    return getShapeTriangleCount(Sphere);
-}
-
-size_t GeometryCache::getCubeTriangleCount() {
-    return getShapeTriangleCount(Cube);
-}
-
 using IndexPair = uint64_t;
 using IndexPairs = std::unordered_set<IndexPair>;
 
@@ -647,83 +634,24 @@ gpu::Stream::FormatPointer& getSolidStreamFormat() {
         SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT);
         SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::TEXCOORD0, gpu::Stream::TEXCOORD0, TEXCOORD0_ELEMENT);
         SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::TANGENT, gpu::Stream::TANGENT, TANGENT_ELEMENT);
+        SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
     }
     return SOLID_STREAM_FORMAT;
 }
 
-gpu::Stream::FormatPointer& getInstancedSolidStreamFormat() {
-    if (!INSTANCED_SOLID_STREAM_FORMAT) {
-        INSTANCED_SOLID_STREAM_FORMAT = std::make_shared<gpu::Stream::Format>(); // 1 for everyone
-        INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, POSITION_ELEMENT);
-        INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT);
-        INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::TEXCOORD0, gpu::Stream::TEXCOORD0, TEXCOORD0_ELEMENT);
-        INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::TANGENT, gpu::Stream::TANGENT, TANGENT_ELEMENT);
-        INSTANCED_SOLID_STREAM_FORMAT->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
-    }
-    return INSTANCED_SOLID_STREAM_FORMAT;
-}
-
-gpu::Stream::FormatPointer& getInstancedSolidFadeStreamFormat() {
-    if (!INSTANCED_SOLID_FADE_STREAM_FORMAT) {
-        INSTANCED_SOLID_FADE_STREAM_FORMAT = std::make_shared<gpu::Stream::Format>(); // 1 for everyone
-        INSTANCED_SOLID_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, POSITION_ELEMENT);
-        INSTANCED_SOLID_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT);
-        INSTANCED_SOLID_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::TEXCOORD0, gpu::Stream::TEXCOORD0, TEXCOORD0_ELEMENT);
-        INSTANCED_SOLID_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::TANGENT, gpu::Stream::TANGENT, TANGENT_ELEMENT);
-        INSTANCED_SOLID_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
-        INSTANCED_SOLID_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::TEXCOORD2, gpu::Stream::TEXCOORD2, TEXCOORD4_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
-        INSTANCED_SOLID_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::TEXCOORD3, gpu::Stream::TEXCOORD3, TEXCOORD4_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
-        INSTANCED_SOLID_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::TEXCOORD4, gpu::Stream::TEXCOORD4, TEXCOORD4_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
-    }
-    return INSTANCED_SOLID_FADE_STREAM_FORMAT;
-}
-
 gpu::Stream::FormatPointer& getWireStreamFormat() {
     if (!WIRE_STREAM_FORMAT) {
         WIRE_STREAM_FORMAT = std::make_shared<gpu::Stream::Format>(); // 1 for everyone
         WIRE_STREAM_FORMAT->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, POSITION_ELEMENT);
         WIRE_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT);
+        WIRE_STREAM_FORMAT->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
     }
     return WIRE_STREAM_FORMAT;
 }
 
-gpu::Stream::FormatPointer& getInstancedWireStreamFormat() {
-    if (!INSTANCED_WIRE_STREAM_FORMAT) {
-        INSTANCED_WIRE_STREAM_FORMAT = std::make_shared<gpu::Stream::Format>(); // 1 for everyone
-        INSTANCED_WIRE_STREAM_FORMAT->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, POSITION_ELEMENT);
-        INSTANCED_WIRE_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT);
-        INSTANCED_WIRE_STREAM_FORMAT->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
-    }
-    return INSTANCED_WIRE_STREAM_FORMAT;
-}
-
-gpu::Stream::FormatPointer& getInstancedWireFadeStreamFormat() {
-    if (!INSTANCED_WIRE_FADE_STREAM_FORMAT) {
-        INSTANCED_WIRE_FADE_STREAM_FORMAT = std::make_shared<gpu::Stream::Format>(); // 1 for everyone
-        INSTANCED_WIRE_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, POSITION_ELEMENT);
-        INSTANCED_WIRE_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, NORMAL_ELEMENT);
-        INSTANCED_WIRE_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, COLOR_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
-        INSTANCED_WIRE_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::TEXCOORD2, gpu::Stream::TEXCOORD2, TEXCOORD4_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
-        INSTANCED_WIRE_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::TEXCOORD3, gpu::Stream::TEXCOORD3, TEXCOORD4_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
-        INSTANCED_WIRE_FADE_STREAM_FORMAT->setAttribute(gpu::Stream::TEXCOORD4, gpu::Stream::TEXCOORD4, TEXCOORD4_ELEMENT, 0, gpu::Stream::PER_INSTANCE);
-    }
-    return INSTANCED_WIRE_FADE_STREAM_FORMAT;
-}
-
-QHash<SimpleProgramKey, gpu::PipelinePointer> GeometryCache::_simplePrograms;
-
-gpu::ShaderPointer GeometryCache::_simpleShader;
-gpu::ShaderPointer GeometryCache::_transparentShader;
-gpu::ShaderPointer GeometryCache::_unlitShader;
-gpu::ShaderPointer GeometryCache::_simpleFadeShader;
-gpu::ShaderPointer GeometryCache::_unlitFadeShader;
-gpu::ShaderPointer GeometryCache::_forwardSimpleShader;
-gpu::ShaderPointer GeometryCache::_forwardTransparentShader;
-gpu::ShaderPointer GeometryCache::_forwardUnlitShader;
-gpu::ShaderPointer GeometryCache::_forwardSimpleFadeShader;
-gpu::ShaderPointer GeometryCache::_forwardUnlitFadeShader;
-
+std::map<std::tuple<bool, bool, bool, bool>, gpu::ShaderPointer> GeometryCache::_shapeShaders;
 std::map<std::tuple<bool, bool, bool, graphics::MaterialKey::CullFaceMode>, render::ShapePipelinePointer> GeometryCache::_shapePipelines;
+QHash<SimpleProgramKey, gpu::PipelinePointer> GeometryCache::_simplePrograms;
 
 GeometryCache::GeometryCache() :
 _nextID(0) {
@@ -809,103 +737,35 @@ render::ShapePipelinePointer GeometryCache::getFadingShapePipeline(bool textured
     );
 }
 
-void GeometryCache::renderShape(gpu::Batch& batch, Shape shape) {
-    batch.setInputFormat(getSolidStreamFormat());
-    _shapes[shape].draw(batch);
-}
-
-void GeometryCache::renderWireShape(gpu::Batch& batch, Shape shape) {
-    batch.setInputFormat(getWireStreamFormat());
-    _shapes[shape].drawWire(batch);
-}
-
-void GeometryCache::renderShape(gpu::Batch& batch, Shape shape, const glm::vec4& color) {
-    batch.setInputFormat(getSolidStreamFormat());
-    batch._glColor4f(color.r, color.g, color.b, color.a);
-    _shapes[shape].draw(batch);
-}
-
-void GeometryCache::renderWireShape(gpu::Batch& batch, Shape shape, const glm::vec4& color) {
-    batch.setInputFormat(getWireStreamFormat());
-    batch._glColor4f(color.r, color.g, color.b, color.a);
-    _shapes[shape].drawWire(batch);
-}
-
-void setupBatchInstance(gpu::Batch& batch, gpu::BufferPointer colorBuffer) {
+void setupColorInputBuffer(gpu::Batch& batch, gpu::BufferPointer colorBuffer) {
     gpu::BufferView colorView(colorBuffer, COLOR_ELEMENT);
     batch.setInputBuffer(gpu::Stream::COLOR, colorView);
 }
 
+void GeometryCache::renderShape(gpu::Batch& batch, Shape shape, gpu::BufferPointer& colorBuffer) {
+    batch.setInputFormat(getSolidStreamFormat());
+    setupColorInputBuffer(batch, colorBuffer);
+    _shapes[shape].draw(batch);
+}
+
+void GeometryCache::renderWireShape(gpu::Batch& batch, Shape shape, gpu::BufferPointer& colorBuffer) {
+    batch.setInputFormat(getWireStreamFormat());
+    setupColorInputBuffer(batch, colorBuffer);
+    _shapes[shape].drawWire(batch);
+}
+
 void GeometryCache::renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer) {
-    batch.setInputFormat(getInstancedSolidStreamFormat());
-    setupBatchInstance(batch, colorBuffer);
+    batch.setInputFormat(getSolidStreamFormat());
+    setupColorInputBuffer(batch, colorBuffer);
     _shapes[shape].drawInstances(batch, count);
 }
 
 void GeometryCache::renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer) {
-    batch.setInputFormat(getInstancedWireStreamFormat());
-    setupBatchInstance(batch, colorBuffer);
+    batch.setInputFormat(getWireStreamFormat());
+    setupColorInputBuffer(batch, colorBuffer);
     _shapes[shape].drawWireInstances(batch, count);
 }
 
-void setupBatchFadeInstance(gpu::Batch& batch, gpu::BufferPointer colorBuffer,
-    gpu::BufferPointer fadeBuffer1, gpu::BufferPointer fadeBuffer2, gpu::BufferPointer fadeBuffer3) {
-    gpu::BufferView colorView(colorBuffer, COLOR_ELEMENT);
-    gpu::BufferView texCoord2View(fadeBuffer1, TEXCOORD4_ELEMENT);
-    gpu::BufferView texCoord3View(fadeBuffer2, TEXCOORD4_ELEMENT);
-    gpu::BufferView texCoord4View(fadeBuffer3, TEXCOORD4_ELEMENT);
-    batch.setInputBuffer(gpu::Stream::COLOR, colorView);
-    batch.setInputBuffer(gpu::Stream::TEXCOORD2, texCoord2View);
-    batch.setInputBuffer(gpu::Stream::TEXCOORD3, texCoord3View);
-    batch.setInputBuffer(gpu::Stream::TEXCOORD4, texCoord4View);
-}
-
-void GeometryCache::renderFadeShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer,
-    gpu::BufferPointer& fadeBuffer1, gpu::BufferPointer& fadeBuffer2, gpu::BufferPointer& fadeBuffer3) {
-    batch.setInputFormat(getInstancedSolidFadeStreamFormat());
-    setupBatchFadeInstance(batch, colorBuffer, fadeBuffer1, fadeBuffer2, fadeBuffer3);
-    _shapes[shape].drawInstances(batch, count);
-}
-
-void GeometryCache::renderWireFadeShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer,
-    gpu::BufferPointer& fadeBuffer1, gpu::BufferPointer& fadeBuffer2, gpu::BufferPointer& fadeBuffer3) {
-    batch.setInputFormat(getInstancedWireFadeStreamFormat());
-    setupBatchFadeInstance(batch, colorBuffer, fadeBuffer1, fadeBuffer2, fadeBuffer3);
-    _shapes[shape].drawWireInstances(batch, count);
-}
-
-void GeometryCache::renderCube(gpu::Batch& batch) {
-    renderShape(batch, Cube);
-}
-
-void GeometryCache::renderWireCube(gpu::Batch& batch) {
-    renderWireShape(batch, Cube);
-}
-
-void GeometryCache::renderCube(gpu::Batch& batch, const glm::vec4& color) {
-    renderShape(batch, Cube, color);
-}
-
-void GeometryCache::renderWireCube(gpu::Batch& batch, const glm::vec4& color) {
-    renderWireShape(batch, Cube, color);
-}
-
-void GeometryCache::renderSphere(gpu::Batch& batch) {
-    renderShape(batch, Sphere);
-}
-
-void GeometryCache::renderWireSphere(gpu::Batch& batch) {
-    renderWireShape(batch, Sphere);
-}
-
-void GeometryCache::renderSphere(gpu::Batch& batch, const glm::vec4& color) {
-    renderShape(batch, Sphere, color);
-}
-
-void GeometryCache::renderWireSphere(gpu::Batch& batch, const glm::vec4& color) {
-    renderWireShape(batch, Sphere, color);
-}
-
 void GeometryCache::renderGrid(gpu::Batch& batch, const glm::vec2& minCorner, const glm::vec2& maxCorner,
         int majorRows, int majorCols, float majorEdge, int minorRows, int minorCols, float minorEdge,
         const glm::vec4& color, bool forward, int id) {
@@ -963,33 +823,30 @@ void GeometryCache::updateVertices(int id, const QVector<glm::vec2>& points, con
 #endif // def WANT_DEBUG
     }
 
-    const int FLOATS_PER_VERTEX = 2 + 3; // vertices + normals
-    const int NUM_POS_COORDS = 2;
-    const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float);
+    const int FLOATS_PER_VERTEX = 2; // vertices
     details.isCreated = true;
     details.vertices = points.size();
     details.vertexSize = FLOATS_PER_VERTEX;
 
     auto verticesBuffer = std::make_shared<gpu::Buffer>();
+    auto normalBuffer = std::make_shared<gpu::Buffer>();
     auto colorBuffer = std::make_shared<gpu::Buffer>();
     auto streamFormat = std::make_shared<gpu::Stream::Format>();
     auto stream = std::make_shared<gpu::BufferStream>();
 
     details.verticesBuffer = verticesBuffer;
+    details.normalBuffer = normalBuffer;
     details.colorBuffer = colorBuffer;
     details.streamFormat = streamFormat;
     details.stream = stream;
 
-    details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ), 0);
-    details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET);
-    // TODO: circle3D overlays use this to define their vertices, so they need tex coords
-    details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
+    details.streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ));
+    details.streamFormat->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0, gpu::Stream::PER_INSTANCE);
+    details.streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
 
-    details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride);
-    details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
-
-    details.vertices = points.size();
-    details.vertexSize = FLOATS_PER_VERTEX;
+    details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::POSITION)._stride);
+    details.stream->addBuffer(details.normalBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::NORMAL)._stride);
+    details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::COLOR)._stride);
 
     float* vertexData = new float[details.vertices * FLOATS_PER_VERTEX];
     float* vertex = vertexData;
@@ -997,7 +854,6 @@ void GeometryCache::updateVertices(int id, const QVector<glm::vec2>& points, con
     int* colorData = new int[details.vertices];
     int* colorDataAt = colorData;
 
-    const glm::vec3 NORMAL(0.0f, -1.0f, 0.0f);
     auto pointCount = points.size();
     auto colorCount = colors.size();
     int compactColor = 0;
@@ -1005,19 +861,16 @@ void GeometryCache::updateVertices(int id, const QVector<glm::vec2>& points, con
         const auto& point = points[i];
         *(vertex++) = point.x;
         *(vertex++) = point.y;
-        *(vertex++) = NORMAL.x;
-        *(vertex++) = NORMAL.y;
-        *(vertex++) = NORMAL.z;
         if (i < colorCount) {
             const auto& color = colors[i];
-            compactColor = ((int(color.x * 255.0f) & 0xFF)) |
-                ((int(color.y * 255.0f) & 0xFF) << 8) |
-                ((int(color.z * 255.0f) & 0xFF) << 16) |
-                ((int(color.w * 255.0f) & 0xFF) << 24);
+            compactColor = GeometryCache::toCompactColor(color);
         }
         *(colorDataAt++) = compactColor;
     }
+
     details.verticesBuffer->append(sizeof(float) * FLOATS_PER_VERTEX * details.vertices, (gpu::Byte*) vertexData);
+    const glm::vec3 NORMAL(0.0f, -1.0f, 0.0f);
+    details.normalBuffer->append(3 * sizeof(float), (gpu::Byte*) glm::value_ptr(NORMAL));
     details.colorBuffer->append(sizeof(int) * details.vertices, (gpu::Byte*) colorData);
     delete[] vertexData;
     delete[] colorData;
@@ -1040,32 +893,30 @@ void GeometryCache::updateVertices(int id, const QVector<glm::vec3>& points, con
 #endif // def WANT_DEBUG
     }
 
-    const int FLOATS_PER_VERTEX = 3 + 3; // vertices + normals
-    const int NUM_POS_COORDS = 3;
-    const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float);
+    const int FLOATS_PER_VERTEX = 3; // vertices
     details.isCreated = true;
     details.vertices = points.size();
     details.vertexSize = FLOATS_PER_VERTEX;
 
     auto verticesBuffer = std::make_shared<gpu::Buffer>();
+    auto normalBuffer = std::make_shared<gpu::Buffer>();
     auto colorBuffer = std::make_shared<gpu::Buffer>();
     auto streamFormat = std::make_shared<gpu::Stream::Format>();
     auto stream = std::make_shared<gpu::BufferStream>();
 
     details.verticesBuffer = verticesBuffer;
+    details.normalBuffer = normalBuffer;
     details.colorBuffer = colorBuffer;
     details.streamFormat = streamFormat;
     details.stream = stream;
 
-    details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0);
-    details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET);
-    details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
+    details.streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
+    details.streamFormat->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0, gpu::Stream::PER_INSTANCE);
+    details.streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
 
-    details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride);
-    details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
-
-    details.vertices = points.size();
-    details.vertexSize = FLOATS_PER_VERTEX;
+    details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::POSITION)._stride);
+    details.stream->addBuffer(details.normalBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::NORMAL)._stride);
+    details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::COLOR)._stride);
 
     // Default to white
     int compactColor = 0xFFFFFFFF;
@@ -1075,28 +926,23 @@ void GeometryCache::updateVertices(int id, const QVector<glm::vec3>& points, con
     int* colorData = new int[details.vertices];
     int* colorDataAt = colorData;
 
-    const glm::vec3 NORMAL(0.0f, -1.0f, 0.0f);
     auto pointCount = points.size();
     auto colorCount = colors.size();
     for (auto i = 0; i < pointCount; i++) {
         const glm::vec3& point = points[i];
-        if (i < colorCount) {
-            const glm::vec4& color = colors[i];
-            compactColor = ((int(color.x * 255.0f) & 0xFF)) |
-                ((int(color.y * 255.0f) & 0xFF) << 8) |
-                ((int(color.z * 255.0f) & 0xFF) << 16) |
-                ((int(color.w * 255.0f) & 0xFF) << 24);
-        }
         *(vertex++) = point.x;
         *(vertex++) = point.y;
         *(vertex++) = point.z;
-        *(vertex++) = NORMAL.x;
-        *(vertex++) = NORMAL.y;
-        *(vertex++) = NORMAL.z;
+        if (i < colorCount) {
+            const glm::vec4& color = colors[i];
+            compactColor = GeometryCache::toCompactColor(color);
+        }
         *(colorDataAt++) = compactColor;
     }
 
     details.verticesBuffer->append(sizeof(float) * FLOATS_PER_VERTEX * details.vertices, (gpu::Byte*) vertexData);
+    const glm::vec3 NORMAL(0.0f, -1.0f, 0.0f);
+    details.normalBuffer->append(3 * sizeof(float), (gpu::Byte*) glm::value_ptr(NORMAL));
     details.colorBuffer->append(sizeof(int) * details.vertices, (gpu::Byte*) colorData);
     delete[] vertexData;
     delete[] colorData;
@@ -1120,69 +966,55 @@ void GeometryCache::updateVertices(int id, const QVector<glm::vec3>& points, con
 #endif // def WANT_DEBUG
     }
 
-    const int FLOATS_PER_VERTEX = 3 + 3 + 2; // vertices + normals + tex coords
+    const int FLOATS_PER_VERTEX = 3 + 2; // vertices + tex coords
     const int NUM_POS_COORDS = 3;
-    const int NUM_NORMAL_COORDS = 3;
-    const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float);
-    const int VERTEX_TEX_OFFSET = VERTEX_NORMAL_OFFSET + NUM_NORMAL_COORDS * sizeof(float);
+    const int VERTEX_TEX_OFFSET = NUM_POS_COORDS * sizeof(float);
     details.isCreated = true;
     details.vertices = points.size();
     details.vertexSize = FLOATS_PER_VERTEX;
 
     auto verticesBuffer = std::make_shared<gpu::Buffer>();
+    auto normalBuffer = std::make_shared<gpu::Buffer>();
     auto colorBuffer = std::make_shared<gpu::Buffer>();
     auto streamFormat = std::make_shared<gpu::Stream::Format>();
     auto stream = std::make_shared<gpu::BufferStream>();
 
     details.verticesBuffer = verticesBuffer;
+    details.normalBuffer = normalBuffer;
     details.colorBuffer = colorBuffer;
     details.streamFormat = streamFormat;
     details.stream = stream;
 
-    details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0);
-    details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET);
+    details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
     details.streamFormat->setAttribute(gpu::Stream::TEXCOORD, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV), VERTEX_TEX_OFFSET);
-    details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
+    details.streamFormat->setAttribute(gpu::Stream::NORMAL, 1, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0, gpu::Stream::PER_INSTANCE);
+    details.streamFormat->setAttribute(gpu::Stream::COLOR, 2, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), 0, gpu::Stream::PER_INSTANCE);
 
     details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride);
-    details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
+    details.stream->addBuffer(details.normalBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
+    details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(2)._stride);
 
     assert(points.size() == texCoords.size());
 
-    details.vertices = points.size();
-    details.vertexSize = FLOATS_PER_VERTEX;
-
-    int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
-        ((int(color.y * 255.0f) & 0xFF) << 8) |
-        ((int(color.z * 255.0f) & 0xFF) << 16) |
-        ((int(color.w * 255.0f) & 0xFF) << 24);
-
     float* vertexData = new float[details.vertices * FLOATS_PER_VERTEX];
     float* vertex = vertexData;
 
-    int* colorData = new int[details.vertices];
-    int* colorDataAt = colorData;
-
-    const glm::vec3 NORMAL(0.0f, -1.0f, 0.0f);
     for (int i = 0; i < points.size(); i++) {
         glm::vec3 point = points[i];
         glm::vec2 texCoord = texCoords[i];
         *(vertex++) = point.x;
         *(vertex++) = point.y;
         *(vertex++) = point.z;
-        *(vertex++) = NORMAL.x;
-        *(vertex++) = NORMAL.y;
-        *(vertex++) = NORMAL.z;
         *(vertex++) = texCoord.x;
         *(vertex++) = texCoord.y;
-
-        *(colorDataAt++) = compactColor;
     }
 
-    details.verticesBuffer->append(sizeof(float) * FLOATS_PER_VERTEX * details.vertices, (gpu::Byte*) vertexData);
-    details.colorBuffer->append(sizeof(int) * details.vertices, (gpu::Byte*) colorData);
+    details.verticesBuffer->append(sizeof(float) * FLOATS_PER_VERTEX * details.vertices, (gpu::Byte*)vertexData);
+    const glm::vec3 NORMAL(0.0f, -1.0f, 0.0f);
+    details.normalBuffer->append(3 * sizeof(float), (gpu::Byte*) glm::value_ptr(NORMAL));
+    int compactColor = GeometryCache::toCompactColor(color);
+    details.colorBuffer->append(sizeof(compactColor), (gpu::Byte*) &compactColor);
     delete[] vertexData;
-    delete[] colorData;
 
 #ifdef WANT_DEBUG
     qCDebug(renderutils) << "new registered linestrip buffer made -- _registeredVertices.size():" << _registeredVertices.size();
@@ -1239,12 +1071,11 @@ void GeometryCache::renderBevelCornersRect(gpu::Batch& batch, int x, int y, int
         details.streamFormat = streamFormat;
         details.stream = stream;
 
-        details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ));
-        details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
-
-        details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride);
-        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
+        details.streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ));
+        details.streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), 0, gpu::Stream::PER_INSTANCE);
 
+        details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::POSITION)._stride);
+        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::COLOR)._stride);
 
         float vertexBuffer[NUM_FLOATS]; // only vertices, no normals because we're a 2D quad
         int vertexPoint = 0;
@@ -1283,16 +1114,9 @@ void GeometryCache::renderBevelCornersRect(gpu::Batch& batch, int x, int y, int
         vertexBuffer[vertexPoint++] = x + width;
         vertexBuffer[vertexPoint++] = y + bevelDistance;
 
-        int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
-            ((int(color.y * 255.0f) & 0xFF) << 8) |
-            ((int(color.z * 255.0f) & 0xFF) << 16) |
-            ((int(color.w * 255.0f) & 0xFF) << 24);
-        int colors[NUM_VERTICES] = { compactColor, compactColor, compactColor, compactColor,
-            compactColor, compactColor, compactColor, compactColor };
-
-
         details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer);
-        details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors);
+        int compactColor = GeometryCache::toCompactColor(color);
+        details.colorBuffer->append(sizeof(compactColor), (gpu::Byte*) &compactColor);
     }
 
     batch.setInputFormat(details.streamFormat);
@@ -1322,52 +1146,46 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co
 #endif // def WANT_DEBUG
     }
 
-    const int FLOATS_PER_VERTEX = 2 + 3; // vertices + normals
-    const int VERTICES = 4; // 1 quad = 4 vertices
-    const int NUM_POS_COORDS = 2;
-    const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float);
-
     if (!details.isCreated) {
+        static const int FLOATS_PER_VERTEX = 2; // vertices
+        static const int VERTICES = 4; // 1 quad = 4 vertices
 
         details.isCreated = true;
         details.vertices = VERTICES;
         details.vertexSize = FLOATS_PER_VERTEX;
 
         auto verticesBuffer = std::make_shared<gpu::Buffer>();
+        auto normalBuffer = std::make_shared<gpu::Buffer>();
         auto colorBuffer = std::make_shared<gpu::Buffer>();
         auto streamFormat = std::make_shared<gpu::Stream::Format>();
         auto stream = std::make_shared<gpu::BufferStream>();
 
         details.verticesBuffer = verticesBuffer;
+        details.normalBuffer = normalBuffer;
         details.colorBuffer = colorBuffer;
         details.streamFormat = streamFormat;
         details.stream = stream;
 
-        details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ), 0);
-        details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET);
-        details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
+        details.streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ));
+        details.streamFormat->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0, gpu::Stream::PER_INSTANCE);
+        details.streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), 0, gpu::Stream::PER_INSTANCE);
 
-        details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride);
-        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
+        details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::POSITION)._stride);
+        details.stream->addBuffer(details.normalBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::NORMAL)._stride);
+        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::COLOR)._stride);
 
-
-        const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f);
         float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = {
-            minCorner.x, minCorner.y, NORMAL.x, NORMAL.y, NORMAL.z,
-            maxCorner.x, minCorner.y, NORMAL.x, NORMAL.y, NORMAL.z,
-            minCorner.x, maxCorner.y, NORMAL.x, NORMAL.y, NORMAL.z,
-            maxCorner.x, maxCorner.y, NORMAL.x, NORMAL.y, NORMAL.z,
+            minCorner.x, minCorner.y,
+            maxCorner.x, minCorner.y,
+            minCorner.x, maxCorner.y,
+            maxCorner.x, maxCorner.y,
         };
 
-        const int NUM_COLOR_SCALARS_PER_QUAD = 4;
-        int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
-            ((int(color.y * 255.0f) & 0xFF) << 8) |
-            ((int(color.z * 255.0f) & 0xFF) << 16) |
-            ((int(color.w * 255.0f) & 0xFF) << 24);
-        int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor };
-
         details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer);
-        details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors);
+        const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f);
+        details.normalBuffer->append(3 * sizeof(float), (gpu::Byte*) glm::value_ptr(NORMAL));
+        int compactColor = GeometryCache::toCompactColor(color);
+        details.colorBuffer->append(sizeof(compactColor), (gpu::Byte*) &compactColor);
     }
 
     batch.setInputFormat(details.streamFormat);
@@ -1409,59 +1227,49 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec2& minCorner, co
 #endif // def WANT_DEBUG
     }
 
-    const int FLOATS_PER_VERTEX = 2 + 3 + 2; // vertices + normals + tex coords
-    const int VERTICES = 4; // 1 quad = 4 vertices
-    const int NUM_POS_COORDS = 2;
-    const int NUM_NORMAL_COORDS = 3;
-    const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float);
-    const int VERTEX_TEXCOORD_OFFSET = VERTEX_NORMAL_OFFSET + NUM_NORMAL_COORDS * sizeof(float);
-
     if (!details.isCreated) {
+        static const int FLOATS_PER_VERTEX = 2 + 2; // vertices + tex coords
+        static const int VERTICES = 4; // 1 quad = 4 vertices
+        static const int NUM_POS_COORDS = 2;
+        static const int VERTEX_TEXCOORD_OFFSET = NUM_POS_COORDS * sizeof(float);
 
         details.isCreated = true;
         details.vertices = VERTICES;
         details.vertexSize = FLOATS_PER_VERTEX;
 
         auto verticesBuffer = std::make_shared<gpu::Buffer>();
+        auto normalBuffer = std::make_shared<gpu::Buffer>();
         auto colorBuffer = std::make_shared<gpu::Buffer>();
-
         auto streamFormat = std::make_shared<gpu::Stream::Format>();
         auto stream = std::make_shared<gpu::BufferStream>();
 
         details.verticesBuffer = verticesBuffer;
+        details.normalBuffer = normalBuffer;
         details.colorBuffer = colorBuffer;
-
         details.streamFormat = streamFormat;
         details.stream = stream;
 
-        // zzmp: fix the normal across all renderQuad
-        details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ), 0);
-        details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET);
+        details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::XYZ));
         details.streamFormat->setAttribute(gpu::Stream::TEXCOORD, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV), VERTEX_TEXCOORD_OFFSET);
-        details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
+        details.streamFormat->setAttribute(gpu::Stream::NORMAL, 1, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0, gpu::Stream::PER_INSTANCE);
+        details.streamFormat->setAttribute(gpu::Stream::COLOR, 2, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), 0, gpu::Stream::PER_INSTANCE);
 
         details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride);
-        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
+        details.stream->addBuffer(details.normalBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
+        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(2)._stride);
 
-
-        const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f);
         float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = {
-            minCorner.x, minCorner.y, NORMAL.x, NORMAL.y, NORMAL.z, texCoordMinCorner.x, texCoordMinCorner.y,
-            maxCorner.x, minCorner.y, NORMAL.x, NORMAL.y, NORMAL.z, texCoordMaxCorner.x, texCoordMinCorner.y,
-            minCorner.x, maxCorner.y, NORMAL.x, NORMAL.y, NORMAL.z, texCoordMinCorner.x, texCoordMaxCorner.y,
-            maxCorner.x, maxCorner.y, NORMAL.x, NORMAL.y, NORMAL.z, texCoordMaxCorner.x, texCoordMaxCorner.y,
+            minCorner.x, minCorner.y, texCoordMinCorner.x, texCoordMinCorner.y,
+            maxCorner.x, minCorner.y, texCoordMaxCorner.x, texCoordMinCorner.y,
+            minCorner.x, maxCorner.y, texCoordMinCorner.x, texCoordMaxCorner.y,
+            maxCorner.x, maxCorner.y, texCoordMaxCorner.x, texCoordMaxCorner.y,
         };
 
-
-        const int NUM_COLOR_SCALARS_PER_QUAD = 4;
-        int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
-            ((int(color.y * 255.0f) & 0xFF) << 8) |
-            ((int(color.z * 255.0f) & 0xFF) << 16) |
-            ((int(color.w * 255.0f) & 0xFF) << 24);
-        int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor };
-
         details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer);
-        details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors);
+        const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f);
+        details.normalBuffer->append(3 * sizeof(float), (gpu::Byte*) glm::value_ptr(NORMAL));
+        int compactColor = GeometryCache::toCompactColor(color);
+        details.colorBuffer->append(sizeof(compactColor), (gpu::Byte*) &compactColor);
     }
 
     batch.setInputFormat(details.streamFormat);
@@ -1491,54 +1299,46 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& minCorner, co
 #endif // def WANT_DEBUG
     }
 
-    const int FLOATS_PER_VERTEX = 3 + 3; // vertices + normals
-    const int VERTICES = 4; // 1 quad = 4 vertices
-    const int NUM_POS_COORDS = 3;
-    const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float);
-
     if (!details.isCreated) {
+        static const int FLOATS_PER_VERTEX = 3; // vertices
+        static const int VERTICES = 4; // 1 quad = 4 vertices
 
         details.isCreated = true;
         details.vertices = VERTICES;
         details.vertexSize = FLOATS_PER_VERTEX;
 
         auto verticesBuffer = std::make_shared<gpu::Buffer>();
+        auto normalBuffer = std::make_shared<gpu::Buffer>();
         auto colorBuffer = std::make_shared<gpu::Buffer>();
-
         auto streamFormat = std::make_shared<gpu::Stream::Format>();
         auto stream = std::make_shared<gpu::BufferStream>();
 
         details.verticesBuffer = verticesBuffer;
+        details.normalBuffer = normalBuffer;
         details.colorBuffer = colorBuffer;
-
         details.streamFormat = streamFormat;
         details.stream = stream;
 
-        details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0);
-        details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET);
-        details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
+        details.streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
+        details.streamFormat->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0, gpu::Stream::PER_INSTANCE);
+        details.streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), 0, gpu::Stream::PER_INSTANCE);
 
-        details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride);
-        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
+        details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::POSITION)._stride);
+        details.stream->addBuffer(details.normalBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::NORMAL)._stride);
+        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::COLOR)._stride);
 
-
-        const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f);
         float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = {
-            minCorner.x, minCorner.y, minCorner.z, NORMAL.x, NORMAL.y, NORMAL.z,
-            maxCorner.x, minCorner.y, minCorner.z, NORMAL.x, NORMAL.y, NORMAL.z,
-            minCorner.x, maxCorner.y, maxCorner.z, NORMAL.x, NORMAL.y, NORMAL.z,
-            maxCorner.x, maxCorner.y, maxCorner.z, NORMAL.x, NORMAL.y, NORMAL.z,
+            minCorner.x, minCorner.y, minCorner.z,
+            maxCorner.x, minCorner.y, minCorner.z,
+            minCorner.x, maxCorner.y, maxCorner.z,
+            maxCorner.x, maxCorner.y, maxCorner.z,
         };
 
-        const int NUM_COLOR_SCALARS_PER_QUAD = 4;
-        int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
-            ((int(color.y * 255.0f) & 0xFF) << 8) |
-            ((int(color.z * 255.0f) & 0xFF) << 16) |
-            ((int(color.w * 255.0f) & 0xFF) << 24);
-        int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor };
-
         details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer);
-        details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors);
+        const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f);
+        details.normalBuffer->append(3 * sizeof(float), (gpu::Byte*) glm::value_ptr(NORMAL));
+        int compactColor = GeometryCache::toCompactColor(color);
+        details.colorBuffer->append(sizeof(compactColor), (gpu::Byte*) &compactColor);
     }
 
     batch.setInputFormat(details.streamFormat);
@@ -1587,56 +1387,49 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons
 #endif // def WANT_DEBUG
     }
 
-    const int FLOATS_PER_VERTEX = 3 + 3 + 2; // vertices + normals + tex coords
-    const int VERTICES = 4; // 1 quad = 4 vertices
-    const int NUM_POS_COORDS = 3;
-    const int NUM_NORMAL_COORDS = 3;
-    const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float);
-    const int VERTEX_TEXCOORD_OFFSET = VERTEX_NORMAL_OFFSET + NUM_NORMAL_COORDS * sizeof(float);
-
-
     if (!details.isCreated) {
+        static const int FLOATS_PER_VERTEX = 3 + 2; // vertices + tex coords
+        static const int VERTICES = 4; // 1 quad = 4 vertices
+        static const int NUM_POS_COORDS = 3;
+        static const int VERTEX_TEXCOORD_OFFSET = NUM_POS_COORDS * sizeof(float);
 
         details.isCreated = true;
         details.vertices = VERTICES;
         details.vertexSize = FLOATS_PER_VERTEX; // NOTE: this isn't used for BatchItemDetails maybe we can get rid of it
 
         auto verticesBuffer = std::make_shared<gpu::Buffer>();
+        auto normalBuffer = std::make_shared<gpu::Buffer>();
         auto colorBuffer = std::make_shared<gpu::Buffer>();
         auto streamFormat = std::make_shared<gpu::Stream::Format>();
         auto stream = std::make_shared<gpu::BufferStream>();
 
-        details.verticesBuffer = verticesBuffer;
+        details.normalBuffer = normalBuffer;
+        details.colorBuffer = colorBuffer;
         details.colorBuffer = colorBuffer;
         details.streamFormat = streamFormat;
         details.stream = stream;
 
-        details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0);
-        details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET);
+        details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
         details.streamFormat->setAttribute(gpu::Stream::TEXCOORD, 0, gpu::Element(gpu::VEC2, gpu::FLOAT, gpu::UV), VERTEX_TEXCOORD_OFFSET);
-        details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
+        details.streamFormat->setAttribute(gpu::Stream::NORMAL, 1, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0, gpu::Stream::PER_INSTANCE);
+        details.streamFormat->setAttribute(gpu::Stream::COLOR, 2, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), 0, gpu::Stream::PER_INSTANCE);
 
         details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride);
-        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
+        details.stream->addBuffer(details.normalBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
+        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(2)._stride);
 
-
-        const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f);
         float vertexBuffer[VERTICES * FLOATS_PER_VERTEX] = {
-            bottomLeft.x, bottomLeft.y, bottomLeft.z, NORMAL.x, NORMAL.y, NORMAL.z, texCoordBottomLeft.x, texCoordBottomLeft.y,
-            bottomRight.x, bottomRight.y, bottomRight.z, NORMAL.x, NORMAL.y, NORMAL.z, texCoordBottomRight.x, texCoordBottomRight.y,
-            topLeft.x, topLeft.y, topLeft.z, NORMAL.x, NORMAL.y, NORMAL.z, texCoordTopLeft.x, texCoordTopLeft.y,
-            topRight.x, topRight.y, topRight.z, NORMAL.x, NORMAL.y, NORMAL.z, texCoordTopRight.x, texCoordTopRight.y,
+            bottomLeft.x, bottomLeft.y, bottomLeft.z, texCoordBottomLeft.x, texCoordBottomLeft.y,
+            bottomRight.x, bottomRight.y, bottomRight.z, texCoordBottomRight.x, texCoordBottomRight.y,
+            topLeft.x, topLeft.y, topLeft.z, texCoordTopLeft.x, texCoordTopLeft.y,
+            topRight.x, topRight.y, topRight.z, texCoordTopRight.x, texCoordTopRight.y,
         };
 
-        const int NUM_COLOR_SCALARS_PER_QUAD = 4;
-        int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
-            ((int(color.y * 255.0f) & 0xFF) << 8) |
-            ((int(color.z * 255.0f) & 0xFF) << 16) |
-            ((int(color.w * 255.0f) & 0xFF) << 24);
-        int colors[NUM_COLOR_SCALARS_PER_QUAD] = { compactColor, compactColor, compactColor, compactColor };
-
         details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer);
-        details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors);
+        const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f);
+        details.normalBuffer->append(3 * sizeof(float), (gpu::Byte*) glm::value_ptr(NORMAL));
+        int compactColor = GeometryCache::toCompactColor(color);
+        details.colorBuffer->append(sizeof(compactColor), (gpu::Byte*) &compactColor);
     }
 
     batch.setInputFormat(details.streamFormat);
@@ -1644,6 +1437,88 @@ void GeometryCache::renderQuad(gpu::Batch& batch, const glm::vec3& topLeft, cons
     batch.draw(gpu::TRIANGLE_STRIP, 4, 0);
 }
 
+void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2,
+    const glm::vec4& color1, const glm::vec4& color2, int id) {
+
+    bool registered = (id != UNKNOWN_ID);
+    Vec3Pair key(p1, p2);
+
+    BatchItemDetails& details = _registeredLine3DVBOs[id];
+
+    // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed
+    if (registered && details.isCreated) {
+        Vec3Pair& lastKey = _lastRegisteredLine3D[id];
+        if (lastKey != key) {
+            details.clear();
+            _lastRegisteredLine3D[id] = key;
+#ifdef WANT_DEBUG
+            qCDebug(renderutils) << "renderLine() 3D ... RELEASING REGISTERED line";
+#endif // def WANT_DEBUG
+        }
+#ifdef WANT_DEBUG
+        else {
+            qCDebug(renderutils) << "renderLine() 3D ... REUSING PREVIOUSLY REGISTERED line";
+        }
+#endif // def WANT_DEBUG
+    }
+
+    if (!details.isCreated) {
+        static const int FLOATS_PER_VERTEX = 3;  // vertices
+        static const int vertices = 2;
+
+        details.isCreated = true;
+        details.vertices = vertices;
+        details.vertexSize = FLOATS_PER_VERTEX;
+
+        auto verticesBuffer = std::make_shared<gpu::Buffer>();
+        auto normalBuffer = std::make_shared<gpu::Buffer>();
+        auto colorBuffer = std::make_shared<gpu::Buffer>();
+        auto streamFormat = std::make_shared<gpu::Stream::Format>();
+        auto stream = std::make_shared<gpu::BufferStream>();
+
+        details.verticesBuffer = verticesBuffer;
+        details.normalBuffer = normalBuffer;
+        details.colorBuffer = colorBuffer;
+        details.streamFormat = streamFormat;
+        details.stream = stream;
+
+        details.streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
+        details.streamFormat->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0, gpu::Stream::PER_INSTANCE);
+        details.streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
+
+        details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::POSITION)._stride);
+        details.stream->addBuffer(details.normalBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::NORMAL)._stride);
+        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::COLOR)._stride);
+
+        float vertexBuffer[vertices * FLOATS_PER_VERTEX] = {
+            p1.x, p1.y, p1.z,
+            p2.x, p2.y, p2.z
+        };
+
+        const int NUM_COLOR_SCALARS = 2;
+        int compactColor1 = GeometryCache::toCompactColor(color1);
+        int compactColor2 = GeometryCache::toCompactColor(color2);
+        int colors[NUM_COLOR_SCALARS] = { compactColor1, compactColor2 };
+
+        details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer);
+        const glm::vec3 NORMAL(0.0f, 0.0f, 1.0f);
+        details.normalBuffer->append(3 * sizeof(float), (gpu::Byte*) glm::value_ptr(NORMAL));
+        details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors);
+
+#ifdef WANT_DEBUG
+        if (id == UNKNOWN_ID) {
+            qCDebug(renderutils) << "new renderLine() 3D VBO made -- _line3DVBOs.size():" << _line3DVBOs.size();
+        } else {
+            qCDebug(renderutils) << "new registered renderLine() 3D VBO made -- _registeredLine3DVBOs.size():" << _registeredLine3DVBOs.size();
+        }
+#endif
+    }
+
+    batch.setInputFormat(details.streamFormat);
+    batch.setInputStream(0, *details.stream);
+    batch.draw(gpu::LINES, 2, 0);
+}
+
 void GeometryCache::renderDashedLine(gpu::Batch& batch, const glm::vec3& start, const glm::vec3& end, const glm::vec4& color,
     const float dash_length, const float gap_length, int id) {
 
@@ -1664,11 +1539,6 @@ void GeometryCache::renderDashedLine(gpu::Batch& batch, const glm::vec3& start,
 
     if (!details.isCreated) {
 
-        int compactColor = ((int(color.x * 255.0f) & 0xFF)) |
-            ((int(color.y * 255.0f) & 0xFF) << 8) |
-            ((int(color.z * 255.0f) & 0xFF) << 16) |
-            ((int(color.w * 255.0f) & 0xFF) << 24);
-
         // draw each line segment with appropriate gaps
         const float SEGMENT_LENGTH = dash_length + gap_length;
         float length = glm::distance(start, end);
@@ -1679,77 +1549,60 @@ void GeometryCache::renderDashedLine(gpu::Batch& batch, const glm::vec3& start,
         glm::vec3 dashVector = segmentVector / SEGMENT_LENGTH * dash_length;
         glm::vec3 gapVector = segmentVector / SEGMENT_LENGTH * gap_length;
 
-        const int FLOATS_PER_VERTEX = 3 + 3; // vertices + normals
-        const int NUM_POS_COORDS = 3;
-        const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float);
+        const int FLOATS_PER_VERTEX = 3; // vertices
+        details.isCreated = true;
         details.vertices = (segmentCountFloor + 1) * 2;
         details.vertexSize = FLOATS_PER_VERTEX;
-        details.isCreated = true;
 
         auto verticesBuffer = std::make_shared<gpu::Buffer>();
+        auto normalBuffer = std::make_shared<gpu::Buffer>();
         auto colorBuffer = std::make_shared<gpu::Buffer>();
         auto streamFormat = std::make_shared<gpu::Stream::Format>();
         auto stream = std::make_shared<gpu::BufferStream>();
 
         details.verticesBuffer = verticesBuffer;
+        details.normalBuffer = normalBuffer;
         details.colorBuffer = colorBuffer;
         details.streamFormat = streamFormat;
         details.stream = stream;
 
-        details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0);
-        details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET);
-        details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
+        details.streamFormat->setAttribute(gpu::Stream::POSITION, gpu::Stream::POSITION, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ));
+        details.streamFormat->setAttribute(gpu::Stream::NORMAL, gpu::Stream::NORMAL, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0, gpu::Stream::PER_INSTANCE);
+        details.streamFormat->setAttribute(gpu::Stream::COLOR, gpu::Stream::COLOR, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), 0, gpu::Stream::PER_INSTANCE);
 
-        details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride);
-        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
-
-        int* colorData = new int[details.vertices];
-        int* colorDataAt = colorData;
+        details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::POSITION)._stride);
+        details.stream->addBuffer(details.normalBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::NORMAL)._stride);
+        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(gpu::Stream::COLOR)._stride);
 
         float* vertexData = new float[details.vertices * FLOATS_PER_VERTEX];
         float* vertex = vertexData;
 
-        const glm::vec3 NORMAL(1.0f, 0.0f, 0.0f);
         glm::vec3 point = start;
         *(vertex++) = point.x;
         *(vertex++) = point.y;
         *(vertex++) = point.z;
-        *(vertex++) = NORMAL.x;
-        *(vertex++) = NORMAL.y;
-        *(vertex++) = NORMAL.z;
-        *(colorDataAt++) = compactColor;
 
         for (int i = 0; i < segmentCountFloor; i++) {
             point += dashVector;
             *(vertex++) = point.x;
             *(vertex++) = point.y;
             *(vertex++) = point.z;
-            *(vertex++) = NORMAL.x;
-            *(vertex++) = NORMAL.y;
-            *(vertex++) = NORMAL.z;
-            *(colorDataAt++) = compactColor;
 
             point += gapVector;
             *(vertex++) = point.x;
             *(vertex++) = point.y;
             *(vertex++) = point.z;
-            *(vertex++) = NORMAL.x;
-            *(vertex++) = NORMAL.y;
-            *(vertex++) = NORMAL.z;
-            *(colorDataAt++) = compactColor;
         }
         *(vertex++) = end.x;
         *(vertex++) = end.y;
         *(vertex++) = end.z;
-        *(vertex++) = NORMAL.x;
-        *(vertex++) = NORMAL.y;
-        *(vertex++) = NORMAL.z;
-        *(colorDataAt++) = compactColor;
 
         details.verticesBuffer->append(sizeof(float) * FLOATS_PER_VERTEX * details.vertices, (gpu::Byte*) vertexData);
-        details.colorBuffer->append(sizeof(int) * details.vertices, (gpu::Byte*) colorData);
+        const glm::vec3 NORMAL(1.0f, 0.0f, 0.0f);
+        details.normalBuffer->append(3 * sizeof(float), (gpu::Byte*) glm::value_ptr(NORMAL));
+        int compactColor = GeometryCache::toCompactColor(color);
+        details.colorBuffer->append(sizeof(compactColor), (gpu::Byte*) &compactColor);
         delete[] vertexData;
-        delete[] colorData;
 
 #ifdef WANT_DEBUG
         if (registered) {
@@ -1770,6 +1623,7 @@ int GeometryCache::BatchItemDetails::population = 0;
 
 GeometryCache::BatchItemDetails::BatchItemDetails() :
 verticesBuffer(NULL),
+normalBuffer(NULL),
 colorBuffer(NULL),
 streamFormat(NULL),
 stream(NULL),
@@ -1784,6 +1638,7 @@ isCreated(false) {
 
 GeometryCache::BatchItemDetails::BatchItemDetails(const GeometryCache::BatchItemDetails& other) :
 verticesBuffer(other.verticesBuffer),
+normalBuffer(other.normalBuffer),
 colorBuffer(other.colorBuffer),
 streamFormat(other.streamFormat),
 stream(other.stream),
@@ -1808,184 +1663,12 @@ void GeometryCache::BatchItemDetails::clear() {
     isCreated = false;
     uniformBuffer.reset();
     verticesBuffer.reset();
+    normalBuffer.reset();
     colorBuffer.reset();
     streamFormat.reset();
     stream.reset();
 }
 
-void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec3& p1, const glm::vec3& p2,
-    const glm::vec4& color1, const glm::vec4& color2, int id) {
-
-    bool registered = (id != UNKNOWN_ID);
-    Vec3Pair key(p1, p2);
-
-    BatchItemDetails& details = _registeredLine3DVBOs[id];
-
-    int compactColor1 = ((int(color1.x * 255.0f) & 0xFF)) |
-        ((int(color1.y * 255.0f) & 0xFF) << 8) |
-        ((int(color1.z * 255.0f) & 0xFF) << 16) |
-        ((int(color1.w * 255.0f) & 0xFF) << 24);
-
-    int compactColor2 = ((int(color2.x * 255.0f) & 0xFF)) |
-        ((int(color2.y * 255.0f) & 0xFF) << 8) |
-        ((int(color2.z * 255.0f) & 0xFF) << 16) |
-        ((int(color2.w * 255.0f) & 0xFF) << 24);
-
-
-    // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed
-    if (registered && details.isCreated) {
-        Vec3Pair& lastKey = _lastRegisteredLine3D[id];
-        if (lastKey != key) {
-            details.clear();
-            _lastRegisteredLine3D[id] = key;
-#ifdef WANT_DEBUG
-            qCDebug(renderutils) << "renderLine() 3D ... RELEASING REGISTERED line";
-#endif // def WANT_DEBUG
-        }
-#ifdef WANT_DEBUG
-        else {
-            qCDebug(renderutils) << "renderLine() 3D ... REUSING PREVIOUSLY REGISTERED line";
-        }
-#endif // def WANT_DEBUG
-    }
-
-    const int FLOATS_PER_VERTEX = 3 + 3; // vertices + normals
-    const int NUM_POS_COORDS = 3;
-    const int VERTEX_NORMAL_OFFSET = NUM_POS_COORDS * sizeof(float);
-    const int vertices = 2;
-    if (!details.isCreated) {
-
-        details.isCreated = true;
-        details.vertices = vertices;
-        details.vertexSize = FLOATS_PER_VERTEX;
-
-        auto verticesBuffer = std::make_shared<gpu::Buffer>();
-        auto colorBuffer = std::make_shared<gpu::Buffer>();
-        auto streamFormat = std::make_shared<gpu::Stream::Format>();
-        auto stream = std::make_shared<gpu::BufferStream>();
-
-        details.verticesBuffer = verticesBuffer;
-        details.colorBuffer = colorBuffer;
-        details.streamFormat = streamFormat;
-        details.stream = stream;
-
-        details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0);
-        details.streamFormat->setAttribute(gpu::Stream::NORMAL, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), VERTEX_NORMAL_OFFSET);
-        details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
-
-        details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride);
-        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
-
-        const glm::vec3 NORMAL(1.0f, 0.0f, 0.0f);
-        float vertexBuffer[vertices * FLOATS_PER_VERTEX] = {
-            p1.x, p1.y, p1.z, NORMAL.x, NORMAL.y, NORMAL.z,
-            p2.x, p2.y, p2.z, NORMAL.x, NORMAL.y, NORMAL.z };
-
-        const int NUM_COLOR_SCALARS = 2;
-        int colors[NUM_COLOR_SCALARS] = { compactColor1, compactColor2 };
-
-        details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer);
-        details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors);
-
-#ifdef WANT_DEBUG
-        if (id == UNKNOWN_ID) {
-            qCDebug(renderutils) << "new renderLine() 3D VBO made -- _line3DVBOs.size():" << _line3DVBOs.size();
-        } else {
-            qCDebug(renderutils) << "new registered renderLine() 3D VBO made -- _registeredLine3DVBOs.size():" << _registeredLine3DVBOs.size();
-        }
-#endif
-    }
-
-    // this is what it takes to render a quad
-    batch.setInputFormat(details.streamFormat);
-    batch.setInputStream(0, *details.stream);
-    batch.draw(gpu::LINES, 2, 0);
-}
-
-void GeometryCache::renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2,
-    const glm::vec4& color1, const glm::vec4& color2, int id) {
-
-    bool registered = (id != UNKNOWN_ID);
-    Vec2Pair key(p1, p2);
-
-    BatchItemDetails& details = _registeredLine2DVBOs[id];
-
-    int compactColor1 = ((int(color1.x * 255.0f) & 0xFF)) |
-        ((int(color1.y * 255.0f) & 0xFF) << 8) |
-        ((int(color1.z * 255.0f) & 0xFF) << 16) |
-        ((int(color1.w * 255.0f) & 0xFF) << 24);
-
-    int compactColor2 = ((int(color2.x * 255.0f) & 0xFF)) |
-        ((int(color2.y * 255.0f) & 0xFF) << 8) |
-        ((int(color2.z * 255.0f) & 0xFF) << 16) |
-        ((int(color2.w * 255.0f) & 0xFF) << 24);
-
-
-    // if this is a registered quad, and we have buffers, then check to see if the geometry changed and rebuild if needed
-    if (registered && details.isCreated) {
-        Vec2Pair& lastKey = _lastRegisteredLine2D[id];
-        if (lastKey != key) {
-            details.clear();
-            _lastRegisteredLine2D[id] = key;
-#ifdef WANT_DEBUG
-            qCDebug(renderutils) << "renderLine() 2D ... RELEASING REGISTERED line";
-#endif // def WANT_DEBUG
-        }
-#ifdef WANT_DEBUG
-        else {
-            qCDebug(renderutils) << "renderLine() 2D ... REUSING PREVIOUSLY REGISTERED line";
-        }
-#endif // def WANT_DEBUG
-    }
-
-    const int FLOATS_PER_VERTEX = 2;
-    const int vertices = 2;
-    if (!details.isCreated) {
-
-        details.isCreated = true;
-        details.vertices = vertices;
-        details.vertexSize = FLOATS_PER_VERTEX;
-
-        auto verticesBuffer = std::make_shared<gpu::Buffer>();
-        auto colorBuffer = std::make_shared<gpu::Buffer>();
-        auto streamFormat = std::make_shared<gpu::Stream::Format>();
-        auto stream = std::make_shared<gpu::BufferStream>();
-
-        details.verticesBuffer = verticesBuffer;
-        details.colorBuffer = colorBuffer;
-        details.streamFormat = streamFormat;
-        details.stream = stream;
-
-        details.streamFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element(gpu::VEC3, gpu::FLOAT, gpu::XYZ), 0);
-        details.streamFormat->setAttribute(gpu::Stream::COLOR, 1, gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA));
-
-        details.stream->addBuffer(details.verticesBuffer, 0, details.streamFormat->getChannels().at(0)._stride);
-        details.stream->addBuffer(details.colorBuffer, 0, details.streamFormat->getChannels().at(1)._stride);
-
-
-        float vertexBuffer[vertices * FLOATS_PER_VERTEX] = { p1.x, p1.y, p2.x, p2.y };
-
-        const int NUM_COLOR_SCALARS = 2;
-        int colors[NUM_COLOR_SCALARS] = { compactColor1, compactColor2 };
-
-        details.verticesBuffer->append(sizeof(vertexBuffer), (gpu::Byte*) vertexBuffer);
-        details.colorBuffer->append(sizeof(colors), (gpu::Byte*) colors);
-
-#ifdef WANT_DEBUG
-        if (id == UNKNOWN_ID) {
-            qCDebug(renderutils) << "new renderLine() 2D VBO made -- _line3DVBOs.size():" << _line2DVBOs.size();
-        } else {
-            qCDebug(renderutils) << "new registered renderLine() 2D VBO made -- _registeredLine2DVBOs.size():" << _registeredLine2DVBOs.size();
-        }
-#endif
-    }
-
-    // this is what it takes to render a quad
-    batch.setInputFormat(details.streamFormat);
-    batch.setInputStream(0, *details.stream);
-    batch.draw(gpu::LINES, 2, 0);
-}
-
 void GeometryCache::useSimpleDrawPipeline(gpu::Batch& batch, bool noBlend) {
     static std::once_flag once;
     std::call_once(once, [&]() {
@@ -2175,24 +1858,24 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp
         std::call_once(once, [&]() {
             using namespace shader::render_utils::program;
 
-            _forwardSimpleShader = gpu::Shader::createProgram(simple_forward);
-            _forwardTransparentShader = gpu::Shader::createProgram(simple_translucent_forward);
-            _forwardUnlitShader = gpu::Shader::createProgram(simple_unlit_forward);
+            _shapeShaders[std::make_tuple(false, false, true, false)] = gpu::Shader::createProgram(simple_forward);
+            _shapeShaders[std::make_tuple(true, false, true, false)] = gpu::Shader::createProgram(simple_translucent_forward);
+            _shapeShaders[std::make_tuple(false, true, true, false)] = gpu::Shader::createProgram(simple_unlit_forward);
 
-            _simpleShader = gpu::Shader::createProgram(simple);
-            _transparentShader = gpu::Shader::createProgram(simple_translucent);
-            _unlitShader = gpu::Shader::createProgram(simple_unlit);
+            _shapeShaders[std::make_tuple(false, false, false, false)] = gpu::Shader::createProgram(simple);
+            _shapeShaders[std::make_tuple(true, false, false, false)] = gpu::Shader::createProgram(simple_translucent);
+            _shapeShaders[std::make_tuple(false, true, false, false)] = gpu::Shader::createProgram(simple_unlit);
         });
     } else {
         static std::once_flag once;
         std::call_once(once, [&]() {
             using namespace shader::render_utils::program;
             // Fading is currently disabled during forward rendering
-            _forwardSimpleFadeShader = gpu::Shader::createProgram(simple_forward);
-            _forwardUnlitFadeShader = gpu::Shader::createProgram(simple_unlit_forward);
+            _shapeShaders[std::make_tuple(false, false, true, true)] = gpu::Shader::createProgram(simple_forward);
+            _shapeShaders[std::make_tuple(false, true, true, true)] = gpu::Shader::createProgram(simple_unlit_forward);
 
-            _simpleFadeShader = gpu::Shader::createProgram(simple_fade);
-            _unlitFadeShader = gpu::Shader::createProgram(simple_unlit_fade);
+            _shapeShaders[std::make_tuple(false, false, false, true)] = gpu::Shader::createProgram(simple_fade);
+            _shapeShaders[std::make_tuple(false, true, false, true)] = gpu::Shader::createProgram(simple_unlit_fade);
         });
     }
 
@@ -2220,20 +1903,13 @@ gpu::PipelinePointer GeometryCache::getSimplePipeline(bool textured, bool transp
         PrepareStencil::testMaskDrawShapeNoAA(*state);
     }
 
-    gpu::ShaderPointer program;
-    if (config.isForward()) {
-        program = (config.isUnlit()) ? (config.isFading() ? _forwardUnlitFadeShader : _forwardUnlitShader) :
-                                       (config.isFading() ? _forwardSimpleFadeShader : (config.isTransparent() ? _forwardTransparentShader : _forwardSimpleShader));
-    } else {
-        program = (config.isUnlit()) ? (config.isFading() ? _unlitFadeShader : _unlitShader) :
-                                       (config.isFading() ? _simpleFadeShader : (config.isTransparent() ? _transparentShader : _simpleShader));
-    }
+    gpu::ShaderPointer program = _shapeShaders[std::make_tuple(config.isTransparent(), config.isUnlit(), config.isForward(), config.isFading())];
     gpu::PipelinePointer pipeline = gpu::Pipeline::create(program, state);
     _simplePrograms.insert(config, pipeline);
     return pipeline;
 }
 
-uint32_t toCompactColor(const glm::vec4& color) {
+uint32_t GeometryCache::toCompactColor(const glm::vec4& color) {
     uint32_t compactColor = ((int(color.x * 255.0f) & 0xFF)) |
         ((int(color.y * 255.0f) & 0xFF) << 8) |
         ((int(color.z * 255.0f) & 0xFF) << 16) |
@@ -2251,7 +1927,7 @@ void renderInstances(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color
     // Add color to named buffer
     {
         gpu::BufferPointer instanceColorBuffer = batch.getNamedBuffer(instanceName, INSTANCE_COLOR_BUFFER);
-        auto compactColor = toCompactColor(color);
+        const uint32_t compactColor = GeometryCache::toCompactColor(color);
         instanceColorBuffer->append(compactColor);
     }
 
@@ -2268,55 +1944,6 @@ void renderInstances(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color
     });
 }
 
-static const size_t INSTANCE_FADE_BUFFER1 = 1;
-static const size_t INSTANCE_FADE_BUFFER2 = 2;
-static const size_t INSTANCE_FADE_BUFFER3 = 3;
-
-void renderFadeInstances(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, int fadeCategory, float fadeThreshold,
-    const glm::vec3& fadeNoiseOffset, const glm::vec3& fadeBaseOffset, const glm::vec3& fadeBaseInvSize, bool isWire,
-    const render::ShapePipelinePointer& pipeline, GeometryCache::Shape shape) {
-    // Add pipeline to name
-    std::string instanceName = (isWire ? "wire_shapes_" : "solid_shapes_") + std::to_string(shape) + "_" + std::to_string(std::hash<render::ShapePipelinePointer>()(pipeline));
-
-    // Add color to named buffer
-    {
-        gpu::BufferPointer instanceColorBuffer = batch.getNamedBuffer(instanceName, INSTANCE_COLOR_BUFFER);
-        auto compactColor = toCompactColor(color);
-        instanceColorBuffer->append(compactColor);
-    }
-    // Add fade parameters to named buffers
-    {
-        gpu::BufferPointer fadeBuffer1 = batch.getNamedBuffer(instanceName, INSTANCE_FADE_BUFFER1);
-        gpu::BufferPointer fadeBuffer2 = batch.getNamedBuffer(instanceName, INSTANCE_FADE_BUFFER2);
-        gpu::BufferPointer fadeBuffer3 = batch.getNamedBuffer(instanceName, INSTANCE_FADE_BUFFER3);
-        // Pack parameters in 3 vec4s
-        glm::vec4 fadeData1;
-        glm::vec4 fadeData2;
-        glm::vec4 fadeData3;
-        FadeEffect::packToAttributes(fadeCategory, fadeThreshold, fadeNoiseOffset, fadeBaseOffset, fadeBaseInvSize,
-            fadeData1, fadeData2, fadeData3);
-        fadeBuffer1->append(fadeData1);
-        fadeBuffer2->append(fadeData2);
-        fadeBuffer3->append(fadeData3);
-    }
-
-    // Add call to named buffer
-    batch.setupNamedCalls(instanceName, [args, isWire, pipeline, shape](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
-        auto& buffers = data.buffers;
-        batch.setPipeline(pipeline->pipeline);
-        pipeline->prepare(batch, args);
-
-        if (isWire) {
-            DependencyManager::get<GeometryCache>()->renderWireFadeShapeInstances(batch, shape, data.count(),
-                buffers[INSTANCE_COLOR_BUFFER], buffers[INSTANCE_FADE_BUFFER1], buffers[INSTANCE_FADE_BUFFER2], buffers[INSTANCE_FADE_BUFFER3]);
-        }
-        else {
-            DependencyManager::get<GeometryCache>()->renderFadeShapeInstances(batch, shape, data.count(),
-                buffers[INSTANCE_COLOR_BUFFER], buffers[INSTANCE_FADE_BUFFER1], buffers[INSTANCE_FADE_BUFFER2], buffers[INSTANCE_FADE_BUFFER3]);
-        }
-    });
-}
-
 void GeometryCache::renderSolidShapeInstance(RenderArgs* args, gpu::Batch& batch, GeometryCache::Shape shape, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) {
     assert(pipeline != nullptr);
     renderInstances(args, batch, color, false, pipeline, shape);
@@ -2327,74 +1954,11 @@ void GeometryCache::renderWireShapeInstance(RenderArgs* args, gpu::Batch& batch,
     renderInstances(args, batch, color, true, pipeline, shape);
 }
 
-void GeometryCache::renderSolidFadeShapeInstance(RenderArgs* args, gpu::Batch& batch, GeometryCache::Shape shape, const glm::vec4& color,
-    int fadeCategory, float fadeThreshold, const glm::vec3& fadeNoiseOffset, const glm::vec3& fadeBaseOffset, const glm::vec3& fadeBaseInvSize,
-    const render::ShapePipelinePointer& pipeline) {
-    assert(pipeline != nullptr);
-    renderFadeInstances(args, batch, color, fadeCategory, fadeThreshold, fadeNoiseOffset, fadeBaseOffset, fadeBaseInvSize, false, pipeline, shape);
-}
-
-void GeometryCache::renderWireFadeShapeInstance(RenderArgs* args, gpu::Batch& batch, GeometryCache::Shape shape, const glm::vec4& color,
-    int fadeCategory, float fadeThreshold, const glm::vec3& fadeNoiseOffset, const glm::vec3& fadeBaseOffset, const glm::vec3& fadeBaseInvSize,
-    const render::ShapePipelinePointer& pipeline) {
-    assert(pipeline != nullptr);
-    renderFadeInstances(args, batch, color, fadeCategory, fadeThreshold, fadeNoiseOffset, fadeBaseOffset, fadeBaseInvSize, true, pipeline, shape);
-}
-
 void GeometryCache::renderSolidSphereInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) {
     assert(pipeline != nullptr);
     renderInstances(args, batch, color, false, pipeline, GeometryCache::Sphere);
 }
 
-void GeometryCache::renderWireSphereInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) {
-    assert(pipeline != nullptr);
-    renderInstances(args, batch, color, true, pipeline, GeometryCache::Sphere);
-}
-
-// Enable this in a debug build to cause 'box' entities to iterate through all the
-// available shape types, both solid and wireframes
-//#define DEBUG_SHAPES
-
-
-void GeometryCache::renderSolidCubeInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) {
-    assert(pipeline != nullptr);
-#ifdef DEBUG_SHAPES
-    static auto startTime = usecTimestampNow();
-    renderInstances(INSTANCE_NAME, batch, color, pipeline, [](gpu::Batch& batch, gpu::Batch::NamedBatchData& data) {
-
-        auto usecs = usecTimestampNow();
-        usecs -= startTime;
-        auto msecs = usecs / USECS_PER_MSEC;
-        float seconds = msecs;
-        seconds /= MSECS_PER_SECOND;
-        float fractionalSeconds = seconds - floor(seconds);
-        int shapeIndex = (int)seconds;
-
-        // Every second we flip to the next shape.
-        static const int SHAPE_COUNT = 5;
-        GeometryCache::Shape shapes[SHAPE_COUNT] = {
-            GeometryCache::Cube,
-            GeometryCache::Tetrahedron,
-            GeometryCache::Sphere,
-            GeometryCache::Icosahedron,
-            GeometryCache::Line,
-        };
-
-        shapeIndex %= SHAPE_COUNT;
-        GeometryCache::Shape shape = shapes[shapeIndex];
-
-        // For the first half second for a given shape, show the wireframe, for the second half, show the solid.
-        if (fractionalSeconds > 0.5f) {
-            renderInstances(INSTANCE_NAME, batch, color, true, pipeline, shape);
-        } else {
-            renderInstances(INSTANCE_NAME, batch, color, false, pipeline, shape);
-        }
-    });
-#else
-    renderInstances(args, batch, color, false, pipeline, GeometryCache::Cube);
-#endif
-}
-
 void GeometryCache::renderWireCubeInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color, const render::ShapePipelinePointer& pipeline) {
     static const std::string INSTANCE_NAME = __FUNCTION__;
     assert(pipeline != nullptr);
diff --git a/libraries/render-utils/src/GeometryCache.h b/libraries/render-utils/src/GeometryCache.h
index 179d49c076..3f06a6b1a3 100644
--- a/libraries/render-utils/src/GeometryCache.h
+++ b/libraries/render-utils/src/GeometryCache.h
@@ -180,31 +180,10 @@ public:
     void renderShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer);
     void renderWireShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer);
 
-    void renderFadeShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer,
-        gpu::BufferPointer& fadeBuffer1, gpu::BufferPointer& fadeBuffer2, gpu::BufferPointer& fadeBuffer3);
-    void renderWireFadeShapeInstances(gpu::Batch& batch, Shape shape, size_t count, gpu::BufferPointer& colorBuffer,
-        gpu::BufferPointer& fadeBuffer1, gpu::BufferPointer& fadeBuffer2, gpu::BufferPointer& fadeBuffer3);
-
     void renderSolidShapeInstance(RenderArgs* args, gpu::Batch& batch, Shape shape, const glm::vec4& color,
-                                    const render::ShapePipelinePointer& pipeline);
-    void renderSolidShapeInstance(RenderArgs* args, gpu::Batch& batch, Shape shape, const glm::vec3& color,
-                                    const render::ShapePipelinePointer& pipeline) {
-        renderSolidShapeInstance(args, batch, shape, glm::vec4(color, 1.0f), pipeline);
-    }
-
+                                  const render::ShapePipelinePointer& pipeline);
     void renderWireShapeInstance(RenderArgs* args, gpu::Batch& batch, Shape shape, const glm::vec4& color,
         const render::ShapePipelinePointer& pipeline);
-    void renderWireShapeInstance(RenderArgs* args, gpu::Batch& batch, Shape shape, const glm::vec3& color,
-        const render::ShapePipelinePointer& pipeline) {
-        renderWireShapeInstance(args, batch, shape, glm::vec4(color, 1.0f), pipeline);
-    }
-
-    void renderSolidFadeShapeInstance(RenderArgs* args, gpu::Batch& batch, Shape shape, const glm::vec4& color, int fadeCategory, float fadeThreshold,
-        const glm::vec3& fadeNoiseOffset, const glm::vec3& fadeBaseOffset, const glm::vec3& fadeBaseInvSize,
-        const render::ShapePipelinePointer& pipeline);
-    void renderWireFadeShapeInstance(RenderArgs* args, gpu::Batch& batch, Shape shape, const glm::vec4& color, int fadeCategory, float fadeThreshold,
-        const glm::vec3& fadeNoiseOffset, const glm::vec3& fadeBaseOffset, const glm::vec3& fadeBaseInvSize,
-        const render::ShapePipelinePointer& pipeline);
 
     void renderSolidSphereInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color,
                                     const render::ShapePipelinePointer& pipeline);
@@ -213,20 +192,6 @@ public:
         renderSolidSphereInstance(args, batch, glm::vec4(color, 1.0f), pipeline);
     }
 
-    void renderWireSphereInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color,
-                                    const render::ShapePipelinePointer& pipeline);
-    void renderWireSphereInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec3& color,
-                                    const render::ShapePipelinePointer& pipeline) {
-        renderWireSphereInstance(args, batch, glm::vec4(color, 1.0f), pipeline);
-    }
-
-    void renderSolidCubeInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color,
-                                    const render::ShapePipelinePointer& pipeline);
-    void renderSolidCubeInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec3& color,
-                                    const render::ShapePipelinePointer& pipeline) {
-        renderSolidCubeInstance(args, batch, glm::vec4(color, 1.0f), pipeline);
-    }
-
     void renderWireCubeInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec4& color,
                                     const render::ShapePipelinePointer& pipeline);
     void renderWireCubeInstance(RenderArgs* args, gpu::Batch& batch, const glm::vec3& color,
@@ -235,24 +200,10 @@ public:
     }
 
     // Dynamic geometry
-    void renderShape(gpu::Batch& batch, Shape shape);
-    void renderWireShape(gpu::Batch& batch, Shape shape);
-    void renderShape(gpu::Batch& batch, Shape shape, const glm::vec4& color);
-    void renderWireShape(gpu::Batch& batch, Shape shape, const glm::vec4& color);
+    void renderShape(gpu::Batch& batch, Shape shape, gpu::BufferPointer& colorBuffer);
+    void renderWireShape(gpu::Batch& batch, Shape shape, gpu::BufferPointer& colorBuffer);
     size_t getShapeTriangleCount(Shape shape);
 
-    void renderCube(gpu::Batch& batch);
-    void renderWireCube(gpu::Batch& batch);
-    void renderCube(gpu::Batch& batch, const glm::vec4& color);
-    void renderWireCube(gpu::Batch& batch, const glm::vec4& color);
-    size_t getCubeTriangleCount();
-
-    void renderSphere(gpu::Batch& batch);
-    void renderWireSphere(gpu::Batch& batch);
-    void renderSphere(gpu::Batch& batch, const glm::vec4& color);
-    void renderWireSphere(gpu::Batch& batch, const glm::vec4& color);
-    size_t getSphereTriangleCount();
-
     void renderGrid(gpu::Batch& batch, const glm::vec2& minCorner, const glm::vec2& maxCorner,
         int majorRows, int majorCols, float majorEdge,
         int minorRows, int minorCols, float minorEdge,
@@ -262,10 +213,6 @@ public:
 
     void renderUnitQuad(gpu::Batch& batch, const glm::vec4& color, int id);
 
-    void renderUnitQuad(gpu::Batch& batch, int id) {
-        renderUnitQuad(batch, glm::vec4(1), id);
-    }
-
     void renderQuad(gpu::Batch& batch, int x, int y, int width, int height, const glm::vec4& color, int id)
             { renderQuad(batch, glm::vec2(x,y), glm::vec2(x + width, y + height), color, id); }
             
@@ -307,19 +254,6 @@ public:
     void renderDashedLine(gpu::Batch& batch, const glm::vec3& start, const glm::vec3& end, const glm::vec4& color,
                           const float dash_length, const float gap_length, int id);
 
-    void renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2, const glm::vec3& color, int id)
-                    { renderLine(batch, p1, p2, glm::vec4(color, 1.0f), id); }
-
-    void renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2, const glm::vec4& color, int id)
-                    { renderLine(batch, p1, p2, color, color, id); }
-
-    void renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2,
-                    const glm::vec3& color1, const glm::vec3& color2, int id)
-                    { renderLine(batch, p1, p2, glm::vec4(color1, 1.0f), glm::vec4(color2, 1.0f), id); }
-
-    void renderLine(gpu::Batch& batch, const glm::vec2& p1, const glm::vec2& p2,
-                                    const glm::vec4& color1, const glm::vec4& color2, int id);
-
     void updateVertices(int id, const QVector<glm::vec2>& points, const glm::vec4& color);
     void updateVertices(int id, const QVector<glm::vec2>& points, const QVector<glm::vec4>& colors);
     void updateVertices(int id, const QVector<glm::vec3>& points, const glm::vec4& color);
@@ -364,6 +298,8 @@ public:
 
     graphics::MeshPointer meshFromShape(Shape geometryShape, glm::vec3 color);
 
+    static uint32_t toCompactColor(const glm::vec4& color);
+
 private:
 
     GeometryCache();
@@ -397,6 +333,7 @@ private:
     public:
         static int population;
         gpu::BufferPointer verticesBuffer;
+        gpu::BufferPointer normalBuffer;
         gpu::BufferPointer colorBuffer;
         gpu::BufferPointer uniformBuffer;
         gpu::Stream::FormatPointer streamFormat;
@@ -445,18 +382,9 @@ private:
     QHash<int, Vec2FloatPairPair> _lastRegisteredGridBuffer;
     QHash<int, GridBuffer> _registeredGridBuffers;
 
-    // FIXME: clean these up
-    static gpu::ShaderPointer _simpleShader;
-    static gpu::ShaderPointer _transparentShader;
-    static gpu::ShaderPointer _unlitShader;
-    static gpu::ShaderPointer _simpleFadeShader;
-    static gpu::ShaderPointer _unlitFadeShader;
-    static gpu::ShaderPointer _forwardSimpleShader;
-    static gpu::ShaderPointer _forwardTransparentShader;
-    static gpu::ShaderPointer _forwardUnlitShader;
-    static gpu::ShaderPointer _forwardSimpleFadeShader;
-    static gpu::ShaderPointer _forwardUnlitFadeShader;
-
+    // transparent, unlit, forward, fade
+    static std::map<std::tuple<bool, bool, bool, bool>, gpu::ShaderPointer> _shapeShaders;
+    // transparent, unlit, forward
     static std::map<std::tuple<bool, bool, bool, graphics::MaterialKey::CullFaceMode>, render::ShapePipelinePointer> _shapePipelines;
     static QHash<SimpleProgramKey, gpu::PipelinePointer> _simplePrograms;
 
diff --git a/libraries/render-utils/src/GlobalLight.slh b/libraries/render-utils/src/GlobalLight.slh
index 6702270a5a..de8702ea8c 100644
--- a/libraries/render-utils/src/GlobalLight.slh
+++ b/libraries/render-utils/src/GlobalLight.slh
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 2/5/15.
 //  Copyright 2013 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -286,5 +287,78 @@ vec3 evalGlobalLightingAlphaBlended(
 }
 <@endfunc@>
 
+<@if HIFI_USE_MTOON@>
+<@func declareEvalGlobalLightingAlphaBlendedMToon()@>
+
+<$declareLightingAmbient(1, 1, 1)$>
+<$declareLightingDirectional()$>
+
+float linearstep(float a, float b, float t) {
+  return clamp((t - a) / (b - a), 0.0, 1.0);
+}
+
+vec3 evalGlobalLightingAlphaBlendedMToon(
+    mat4 invViewMat, float obscurance, vec3 positionES, vec3 normalWS, vec3 albedo, vec3 fresnel, float metallic, vec3 emissive,
+    float roughness, float opacity, vec3 shade, float shadingShift, float shadingToony, vec3 matcap,
+    vec3 parametricRim, float parametricRimFresnelPower, float parametricRimLift, vec3 rim, float rimMix, BITFIELD matKey)
+{
+    <$prepareGlobalLight(positionES, normalWS)$>
+
+    SurfaceData surfaceWS = initSurfaceData(roughness, fragNormalWS, fragEyeDirWS);
+
+    color += emissive * isEmissiveEnabled();
+
+    // Ambient
+    vec3 ambientDiffuse;
+    vec3 ambientSpecular;
+    evalLightingAmbient(ambientDiffuse, ambientSpecular, lightAmbient, surfaceWS, metallic, fresnel, albedo, obscurance);
+    color += ambientDiffuse;
+    color += evalSpecularWithOpacity(ambientSpecular, opacity);
+
+    // Directional MToon Shading
+    updateSurfaceDataWithLight(surfaceWS, lightDirection);
+
+    float shading = surfaceWS.ndotl;
+    shading += shadingShift; // shadingShift includes both the scalar and texture values
+    shading = linearstep(-1.0 + shadingToony, 1.0 - shadingToony, shading);
+
+    color += lightIrradiance * mix(albedo, shade, shading) * isDirectionalEnabled();
+
+    vec3 worldViewX = normalize(vec3(surfaceWS.eyeDir.z, 0.0, -surfaceWS.eyeDir.x));
+    vec3 worldViewY = cross(surfaceWS.eyeDir, worldViewX);
+    vec2 matcapUV = vec2(dot(worldViewX, surfaceWS.normal), dot(worldViewY, surfaceWS.normal)) * 0.495 + 0.5;
+    const float epsilon = 0.00001;
+
+    vec3 rimDiffuse;
+    <$evalMaterialMatcap(matcapUV, matcap, matKey, rimDiffuse)$>;
+    float rimColor = clamp(1.0 - dot(surfaceWS.normal, surfaceWS.eyeDir) + parametricRimLift, 0.0, 1.0);
+    rimColor = pow(rimColor, max(parametricRimFresnelPower, epsilon));
+    rimDiffuse += rimColor * parametricRim;
+    rimDiffuse *= rim;
+    rimDiffuse = rimDiffuse * mix(vec3(1.0), vec3(0.0), rimMix);
+    color += rimDiffuse;
+
+    // Haze
+    if (isHazeEnabled() > 0.0) {
+        if ((hazeParams.hazeMode & HAZE_MODE_IS_KEYLIGHT_ATTENUATED) == HAZE_MODE_IS_KEYLIGHT_ATTENUATED) {
+            color = computeHazeColorKeyLightAttenuation(color, lightDirection, fragPositionWS);
+        }
+
+        if ((hazeParams.hazeMode & HAZE_MODE_IS_ACTIVE) == HAZE_MODE_IS_ACTIVE) {
+            vec4 hazeColor = computeHazeColor(
+                positionES,                     // fragment position in eye   coordinates
+                fragPositionWS,                 // fragment position in world coordinates
+                invViewMat[3].xyz,              // eye      position in world coordinates
+                lightDirection                  // keylight direction vector in world coordinates
+            );
+
+            color = mix(color.rgb, hazeColor.rgb, hazeColor.a);
+        }
+    }
+
+    return color;
+}
+<@endfunc@>
+<@endif@>
 
 <@endif@>
diff --git a/libraries/render-utils/src/HighlightEffect.cpp b/libraries/render-utils/src/HighlightEffect.cpp
index b4aced626d..754c99c425 100644
--- a/libraries/render-utils/src/HighlightEffect.cpp
+++ b/libraries/render-utils/src/HighlightEffect.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Olivier Prat on 08/08/17.
 //  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -40,6 +41,7 @@ namespace gr {
 #define OUTLINE_STENCIL_MASK    1
 
 extern void initZPassPipelines(ShapePlumber& plumber, gpu::StatePointer state, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter);
+extern void sortAndRenderZPassShapes(const ShapePlumberPointer& shapePlumber, const render::RenderContextPointer& renderContext, const render::ShapeBounds& inShapes, render::ItemBounds &itemBounds);
 
 HighlightResources::HighlightResources() {
 }
@@ -108,10 +110,14 @@ PrepareDrawHighlight::PrepareDrawHighlight() {
 }
 
 void PrepareDrawHighlight::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) {
-    auto destinationFrameBuffer = inputs;
+    RenderArgs* args = renderContext->args;
 
+    auto destinationFrameBuffer = inputs;
     _resources->update(destinationFrameBuffer);
-    outputs = _resources;
+    outputs.edit0() = _resources;
+
+    outputs.edit1() = args->_renderMode;
+    args->_renderMode = RenderArgs::SHADOW_RENDER_MODE;
 }
 
 gpu::PipelinePointer DrawHighlightMask::_stencilMaskPipeline;
@@ -188,61 +194,7 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c
             batch.setProjectionJitter(jitter.x, jitter.y);
             batch.setViewTransform(viewMat);
 
-            const std::vector<ShapeKey::Builder> keys = {
-                ShapeKey::Builder(), ShapeKey::Builder().withFade(),
-                ShapeKey::Builder().withDeformed(), ShapeKey::Builder().withDeformed().withFade(),
-                ShapeKey::Builder().withDeformed().withDualQuatSkinned(), ShapeKey::Builder().withDeformed().withDualQuatSkinned().withFade(),
-                ShapeKey::Builder().withOwnPipeline(), ShapeKey::Builder().withOwnPipeline().withFade(),
-                ShapeKey::Builder().withDeformed().withOwnPipeline(), ShapeKey::Builder().withDeformed().withOwnPipeline().withFade(),
-                ShapeKey::Builder().withDeformed().withDualQuatSkinned().withOwnPipeline(), ShapeKey::Builder().withDeformed().withDualQuatSkinned().withOwnPipeline().withFade(),
-            };
-            std::vector<std::vector<ShapeKey>> sortedShapeKeys(keys.size());
-
-            const int OWN_PIPELINE_INDEX = 6;
-            for (const auto& items : inShapes) {
-                itemBounds.insert(itemBounds.end(), items.second.begin(), items.second.end());
-
-                int index = items.first.hasOwnPipeline() ? OWN_PIPELINE_INDEX : 0;
-                if (items.first.isDeformed()) {
-                    index += 2;
-                    if (items.first.isDualQuatSkinned()) {
-                        index += 2;
-                    }
-                }
-
-                if (items.first.isFaded()) {
-                    index += 1;
-                }
-
-                sortedShapeKeys[index].push_back(items.first);
-            }
-
-            // Render non-withOwnPipeline things
-            for (size_t i = 0; i < OWN_PIPELINE_INDEX; i++) {
-                auto& shapeKeys = sortedShapeKeys[i];
-                if (shapeKeys.size() > 0) {
-                    const auto& shapePipeline = _shapePlumber->pickPipeline(args, keys[i]);
-                    args->_shapePipeline = shapePipeline;
-                    for (const auto& key : shapeKeys) {
-                        renderShapes(renderContext, _shapePlumber, inShapes.at(key));
-                    }
-                }
-            }
-
-            // Render withOwnPipeline things
-            for (size_t i = OWN_PIPELINE_INDEX; i < keys.size(); i++) {
-                auto& shapeKeys = sortedShapeKeys[i];
-                if (shapeKeys.size() > 0) {
-                    args->_shapePipeline = nullptr;
-                    for (const auto& key : shapeKeys) {
-                        args->_itemShapeKey = key._flags.to_ulong();
-                        renderShapes(renderContext, _shapePlumber, inShapes.at(key));
-                    }
-                }
-            }
-
-            args->_shapePipeline = nullptr;
-            args->_batch = nullptr;
+            sortAndRenderZPassShapes(_shapePlumber, renderContext, inShapes, itemBounds);
         });
 
         _boundsBuffer->setData(itemBounds.size() * sizeof(render::ItemBound), (const gpu::Byte*) itemBounds.data());
@@ -452,13 +404,28 @@ const gpu::PipelinePointer& DebugHighlight::getDepthPipeline() {
     return _depthPipeline;
 }
 
-void SelectionToHighlight::run(const render::RenderContextPointer& renderContext, Outputs& outputs) {
+void SelectionToHighlight::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) {
     auto scene = renderContext->_scene;
     auto highlightStage = scene->getStage<render::HighlightStage>(render::HighlightStage::getName());
 
-    outputs.clear();
+    auto outlines = inputs.get0();
+    auto framebuffer = inputs.get1();
+
+    outputs.edit0().clear();
+    outputs.edit1().clear();
     _sharedParameters->_highlightIds.fill(render::HighlightStage::INVALID_INDEX);
 
+    outputs.edit1().reserve(outlines.size());
+    for (auto item : outlines) {
+        render::HighlightStyle style = scene->getOutlineStyle(item.id, renderContext->args->getViewFrustum() , framebuffer->getHeight());
+        auto selectionName = "__OUTLINE_MATERIAL" + style.toString();
+        if (highlightStage->getHighlightIdBySelection(selectionName) == HighlightStage::INVALID_INDEX) {
+            HighlightStage::Index newIndex = highlightStage->addHighlight(selectionName, style);
+            outputs.edit1().push_back(newIndex);
+        }
+        scene->addItemToSelection(selectionName, item.id);
+    }
+
     int numLayers = 0;
     auto highlightList = highlightStage->getActiveHighlightIds();
 
@@ -467,8 +434,8 @@ void SelectionToHighlight::run(const render::RenderContextPointer& renderContext
 
         if (!scene->isSelectionEmpty(highlight._selectionName)) {
             auto highlightId = highlightStage->getHighlightIdBySelection(highlight._selectionName);
-            _sharedParameters->_highlightIds[outputs.size()] = highlightId;
-            outputs.emplace_back(highlight._selectionName);
+            _sharedParameters->_highlightIds[outputs.edit0().size()] = highlightId;
+            outputs.edit0().emplace_back(highlight._selectionName);
             numLayers++;
 
             if (numLayers == HighlightSharedParameters::MAX_PASS_COUNT) {
@@ -500,6 +467,7 @@ void DrawHighlightTask::configure(const Config& config) {
 
 void DrawHighlightTask::build(JobModel& task, const render::Varying& inputs, render::Varying& outputs) {
     const auto items = inputs.getN<Inputs>(0).get<RenderFetchCullSortTask::BucketList>();
+        const auto& outlines = items[RenderFetchCullSortTask::OUTLINE];
     const auto sceneFrameBuffer = inputs.getN<Inputs>(1);
     const auto primaryFramebuffer = inputs.getN<Inputs>(2);
     const auto deferredFrameTransform = inputs.getN<Inputs>(3);
@@ -510,25 +478,27 @@ void DrawHighlightTask::build(JobModel& task, const render::Varying& inputs, ren
     static std::once_flag once;
     std::call_once(once, [] {
         auto state = std::make_shared<gpu::State>();
-        state->setDepthTest(true, true, gpu::LESS_EQUAL);
         state->setColorWriteMask(false, false, false, false);
-
-
         auto fadeEffect = DependencyManager::get<FadeEffect>();
         initZPassPipelines(*shapePlumber, state, fadeEffect->getBatchSetter(), fadeEffect->getItemUniformSetter());
     });
     auto sharedParameters = std::make_shared<HighlightSharedParameters>();
 
-    const auto highlightSelectionNames = task.addJob<SelectionToHighlight>("SelectionToHighlight", sharedParameters);
+    const auto selectionToHighlightInputs = SelectionToHighlight::Inputs(outlines, primaryFramebuffer).asVarying();
+    const auto highlightSelectionOutputs = task.addJob<SelectionToHighlight>("SelectionToHighlight", selectionToHighlightInputs, sharedParameters);
 
     // Prepare for highlight group rendering.
-    const auto highlightResources = task.addJob<PrepareDrawHighlight>("PrepareHighlight", primaryFramebuffer);
+    const auto prepareOutputs = task.addJob<PrepareDrawHighlight>("PrepareHighlight", primaryFramebuffer);
+    const auto highlightResources = prepareOutputs.getN<PrepareDrawHighlight::Outputs>(0);
     render::Varying highlight0Rect;
 
+    const auto extractSelectionNameInput = Varying(highlightSelectionOutputs.getN<SelectionToHighlight::Outputs>(0));
     for (auto i = 0; i < HighlightSharedParameters::MAX_PASS_COUNT; i++) {
-        const auto selectionName = task.addJob<ExtractSelectionName>("ExtractSelectionName", highlightSelectionNames, i);
+        const auto selectionName = task.addJob<ExtractSelectionName>("ExtractSelectionName", extractSelectionNameInput, i);
         const auto groupItems = addSelectItemJobs(task, selectionName, items);
-        const auto highlightedItemIDs = task.addJob<render::MetaToSubItems>("HighlightMetaToSubItemIDs", groupItems);
+        const auto highlightedSubItemIDs = task.addJob<render::MetaToSubItems>("HighlightMetaToSubItemIDs", groupItems);
+        const auto appendNonMetaOutlinesInput = AppendNonMetaOutlines::Inputs(highlightedSubItemIDs, groupItems).asVarying();
+        const auto highlightedItemIDs = task.addJob<AppendNonMetaOutlines>("AppendNonMetaOutlines", appendNonMetaOutlinesInput);
         const auto highlightedItems = task.addJob<render::IDsToBounds>("HighlightMetaToSubItems", highlightedItemIDs);
 
         // Sort
@@ -558,6 +528,10 @@ void DrawHighlightTask::build(JobModel& task, const render::Varying& inputs, ren
         task.addJob<DrawHighlight>(name, drawHighlightInputs, i, sharedParameters);
     }
 
+    // Cleanup
+    const auto cleanupInput = HighlightCleanup::Inputs(highlightSelectionOutputs.getN<SelectionToHighlight::Outputs>(1), prepareOutputs.getN<PrepareDrawHighlight::Outputs>(1)).asVarying();
+    task.addJob<HighlightCleanup>("HighlightCleanup", cleanupInput);
+
     // Debug highlight
     const auto debugInputs = DebugHighlight::Inputs(highlightResources, const_cast<const render::Varying&>(highlight0Rect), jitter, primaryFramebuffer).asVarying();
     task.addJob<DebugHighlight>("HighlightDebug", debugInputs);
@@ -577,3 +551,31 @@ const render::Varying DrawHighlightTask::addSelectItemJobs(JobModel& task, const
     return task.addJob<SelectItems>("TransparentSelection", selectItemInput);
 }
 
+void AppendNonMetaOutlines::run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) {
+    auto& scene = renderContext->_scene;
+
+    outputs = inputs.get0();
+
+    const auto& groupItems = inputs.get1();
+    for (auto idBound : groupItems) {
+        auto& item = scene->getItem(idBound.id);
+        if (item.exist() && !item.getKey().isMeta()) {
+            outputs.push_back(idBound.id);
+        }
+    }
+}
+
+
+void HighlightCleanup::run(const render::RenderContextPointer& renderContext, const Inputs& inputs) {
+    auto scene = renderContext->_scene;
+    auto highlightStage = scene->getStage<render::HighlightStage>(render::HighlightStage::getName());
+
+    for (auto index : inputs.get0()) {
+        std::string selectionName = highlightStage->getHighlight(index)._selectionName;
+        highlightStage->removeHighlight(index);
+        scene->removeSelection(selectionName);
+    }
+
+    // Reset the render mode
+    renderContext->args->_renderMode = inputs.get1();
+}
diff --git a/libraries/render-utils/src/HighlightEffect.h b/libraries/render-utils/src/HighlightEffect.h
index 933503fdb5..a55ecdf46d 100644
--- a/libraries/render-utils/src/HighlightEffect.h
+++ b/libraries/render-utils/src/HighlightEffect.h
@@ -4,6 +4,7 @@
 //
 //  Created by Olivier Prat on 08/08/17.
 //  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -65,7 +66,7 @@ using HighlightSharedParametersPointer = std::shared_ptr<HighlightSharedParamete
 class PrepareDrawHighlight {
 public:
     using Inputs = gpu::FramebufferPointer;
-    using Outputs = HighlightResourcesPointer;
+    using Outputs = render::VaryingSet2<HighlightResourcesPointer, render::Args::RenderMode>;
     using JobModel = render::Job::ModelIO<PrepareDrawHighlight, Inputs, Outputs>;
 
     PrepareDrawHighlight();
@@ -81,12 +82,13 @@ private:
 class SelectionToHighlight {
 public:
 
-    using Outputs = std::vector<std::string>;
-    using JobModel = render::Job::ModelO<SelectionToHighlight, Outputs>;
+    using Inputs = render::VaryingSet2<render::ItemBounds, gpu::FramebufferPointer>;
+    using Outputs = render::VaryingSet2<std::vector<std::string>, std::vector<render::HighlightStage::Index>>;
+    using JobModel = render::Job::ModelIO<SelectionToHighlight, Inputs, Outputs>;
 
     SelectionToHighlight(HighlightSharedParametersPointer parameters) : _sharedParameters{ parameters } {}
 
-    void run(const render::RenderContextPointer& renderContext, Outputs& outputs);
+    void run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs);
 
 private:
 
@@ -96,7 +98,7 @@ private:
 class ExtractSelectionName {
 public:
 
-    using Inputs = SelectionToHighlight::Outputs;
+    using Inputs = std::vector<std::string>;
     using Outputs = std::string;
     using JobModel = render::Job::ModelIO<ExtractSelectionName, Inputs, Outputs>;
 
@@ -209,6 +211,25 @@ private:
 
 };
 
+class AppendNonMetaOutlines {
+public:
+    using Inputs = render::VaryingSet2<render::ItemIDs, render::ItemBounds>;
+    using Outputs = render::ItemIDs;
+    using JobModel = render::Job::ModelIO<AppendNonMetaOutlines, Inputs, Outputs>;
+
+    AppendNonMetaOutlines() {}
+
+    void run(const render::RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs);
+};
+
+class HighlightCleanup {
+public:
+    using Inputs = render::VaryingSet2<std::vector<render::HighlightStage::Index>, render::Args::RenderMode>;
+    using JobModel = render::Job::ModelI<HighlightCleanup, Inputs>;
+
+    HighlightCleanup() {}
+
+    void run(const render::RenderContextPointer& renderContext, const Inputs& inputs);
+};
+
 #endif // hifi_render_utils_HighlightEffect_h
-
-
diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp
index 9adeb39e7c..7cd5646db3 100644
--- a/libraries/render-utils/src/MeshPartPayload.cpp
+++ b/libraries/render-utils/src/MeshPartPayload.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 10/3/15.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -225,6 +226,10 @@ void ModelMeshPartPayload::updateKey(const render::ItemKey& key) {
         builder.withMirror();
     }
 
+    if (_drawMaterials.hasOutline()) {
+        builder.withOutline();
+    }
+
     _itemKey = builder.build();
 }
 
@@ -273,11 +278,15 @@ void ModelMeshPartPayload::setShapeKey(bool invalidateShapeKey, PrimitiveMode pr
         if (hasTangents) {
             builder.withTangents();
         }
-        if (hasLightmap) {
-            builder.withLightMap();
-        }
-        if (isUnlit) {
-            builder.withUnlit();
+        if (!_drawMaterials.isMToon()) {
+            if (hasLightmap) {
+                builder.withLightMap();
+            }
+            if (isUnlit) {
+                builder.withUnlit();
+            }
+        } else {
+            builder.withMToon();
         }
         if (material) {
             builder.withCullFaceMode(material->getCullFaceMode());
@@ -352,12 +361,17 @@ void ModelMeshPartPayload::render(RenderArgs* args) {
         outColor = procedural->getColor(outColor);
         procedural->prepare(batch, transform.getTranslation(), transform.getScale(), transform.getRotation(), _created,
                             ProceduralProgramKey(outColor.a < 1.0f, _shapeKey.isDeformed(), _shapeKey.isDualQuatSkinned()));
-        batch._glColor4f(outColor.r, outColor.g, outColor.b, outColor.a);
+
+        const uint32_t compactColor = GeometryCache::toCompactColor(glm::vec4(outColor));
+        _drawMesh->getColorBuffer()->setData(sizeof(compactColor), (const gpu::Byte*)&compactColor);
     } else if (!_itemKey.isMirror()) {
         // apply material properties
         if (RenderPipelines::bindMaterials(_drawMaterials, batch, args->_renderMode, args->_enableTexturing)) {
             args->_details._materialSwitches++;
         }
+
+        const uint32_t compactColor = 0xFFFFFFFF;
+        _drawMesh->getColorBuffer()->setData(sizeof(compactColor), (const gpu::Byte*) &compactColor);
     }
 
     // Draw!
@@ -390,6 +404,11 @@ ItemID ModelMeshPartPayload::computeMirrorView(ViewFrustum& viewFrustum) const {
     return MirrorModeHelpers::computeMirrorView(viewFrustum, transform.getTranslation(), transform.getRotation(), _mirrorMode, _portalExitID);
 }
 
+render::HighlightStyle ModelMeshPartPayload::getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const {
+    return render::HighlightStyle::calculateOutlineStyle(_drawMaterials.getOutlineWidthMode(), _drawMaterials.getOutlineWidth(), _drawMaterials.getOutline(),
+                                                         _parentTransform.getTranslation(), viewFrustum, height);
+}
+
 void ModelMeshPartPayload::setBlendshapeBuffer(const std::unordered_map<int, gpu::BufferPointer>& blendshapeBuffers, const QVector<int>& blendedMeshSizes) {
     if (_meshIndex < blendedMeshSizes.length() && blendedMeshSizes.at(_meshIndex) == _meshNumVertices) {
         auto blendshapeBuffer = blendshapeBuffers.find(_meshIndex);
@@ -446,4 +465,11 @@ template <> ItemID payloadComputeMirrorView(const ModelMeshPartPayload::Pointer&
     }
     return Item::INVALID_ITEM_ID;
 }
+
+template <> HighlightStyle payloadGetOutlineStyle(const ModelMeshPartPayload::Pointer& payload, const ViewFrustum& viewFrustum, const size_t height) {
+    if (payload) {
+        return payload->getOutlineStyle(viewFrustum, height);
+    }
+    return HighlightStyle();
+}
 }
diff --git a/libraries/render-utils/src/MeshPartPayload.h b/libraries/render-utils/src/MeshPartPayload.h
index 7e331a9497..0482212f84 100644
--- a/libraries/render-utils/src/MeshPartPayload.h
+++ b/libraries/render-utils/src/MeshPartPayload.h
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 10/3/15.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -17,6 +18,7 @@
 #include <gpu/Batch.h>
 #include <render/Scene.h>
 #include <graphics/Geometry.h>
+#include <render/HighlightStyle.h>
 
 class ModelMeshPartPayload {
 public:
@@ -62,6 +64,7 @@ public:
     void setPortalExitID(const QUuid& portalExitID) { _portalExitID = portalExitID; }
     bool passesZoneOcclusionTest(const std::unordered_set<QUuid>& containingZones) const;
     render::ItemID computeMirrorView(ViewFrustum& viewFrustum) const;
+    render::HighlightStyle getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const;
 
     void addMaterial(graphics::MaterialLayer material) { _drawMaterials.push(material); }
     void removeMaterial(graphics::MaterialPointer material) { _drawMaterials.remove(material); }
@@ -113,7 +116,7 @@ namespace render {
     template <> void payloadRender(const ModelMeshPartPayload::Pointer& payload, RenderArgs* args);
     template <> bool payloadPassesZoneOcclusionTest(const ModelMeshPartPayload::Pointer& payload, const std::unordered_set<QUuid>& containingZones);
     template <> ItemID payloadComputeMirrorView(const ModelMeshPartPayload::Pointer& payload, ViewFrustum& viewFrustum);
-
+    template <> HighlightStyle payloadGetOutlineStyle(const ModelMeshPartPayload::Pointer& payload, const ViewFrustum& viewFrustum, const size_t height);
 }
 
 #endif // hifi_MeshPartPayload_h
diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp
index 7d622ab489..b7b645dd8a 100644
--- a/libraries/render-utils/src/Model.cpp
+++ b/libraries/render-utils/src/Model.cpp
@@ -1846,8 +1846,8 @@ public:
 };
 
 static void packBlendshapeOffsetTo_Pos_F32_3xSN10_Nor_3xSN10_Tan_3xSN10(glm::uvec4& packed, const BlendshapeOffsetUnpacked& unpacked) {
-    float len = glm::compMax(glm::abs(unpacked.positionOffset));
-    glm::vec3 normalizedPos(unpacked.positionOffset);
+    float len = max(abs(unpacked.positionOffsetX), max(abs(unpacked.positionOffsetY), abs(unpacked.positionOffsetZ)));
+    glm::vec3 normalizedPos(unpacked.positionOffsetX, unpacked.positionOffsetY, unpacked.positionOffsetZ);
     if (len > 0.0f) {
         normalizedPos /= len;
     } else {
@@ -1857,8 +1857,8 @@ static void packBlendshapeOffsetTo_Pos_F32_3xSN10_Nor_3xSN10_Tan_3xSN10(glm::uve
     packed = glm::uvec4(
         glm::floatBitsToUint(len),
         glm_packSnorm3x10_1x2(glm::vec4(normalizedPos, 0.0f)),
-        glm_packSnorm3x10_1x2(glm::vec4(unpacked.normalOffset, 0.0f)),
-        glm_packSnorm3x10_1x2(glm::vec4(unpacked.tangentOffset, 0.0f))
+        glm_packSnorm3x10_1x2(glm::vec4(unpacked.normalOffsetX, unpacked.normalOffsetY, unpacked.normalOffsetZ, 0.0f)),
+        glm_packSnorm3x10_1x2(glm::vec4(unpacked.tangentOffsetX, unpacked.tangentOffsetY, unpacked.tangentOffsetZ, 0.0f))
     );
 }
 
@@ -1966,10 +1966,19 @@ void Blender::run() {
                 int index = blendshape.indices.at(j);
 
                 auto& currentBlendshapeOffset = unpackedBlendshapeOffsets[index];
-                currentBlendshapeOffset.positionOffset += blendshape.vertices.at(j) * vertexCoefficient;
-                currentBlendshapeOffset.normalOffset += blendshape.normals.at(j) * normalCoefficient;
+                glm::vec3 blendshapePosition = blendshape.vertices.at(j) * vertexCoefficient;
+                currentBlendshapeOffset.positionOffsetX += blendshapePosition.x;
+                currentBlendshapeOffset.positionOffsetY += blendshapePosition.y;
+                currentBlendshapeOffset.positionOffsetZ += blendshapePosition.z;
+                glm::vec3 blendshapeNormal = blendshape.normals.at(j) * normalCoefficient;
+                currentBlendshapeOffset.normalOffsetX += blendshapeNormal.x;
+                currentBlendshapeOffset.normalOffsetY += blendshapeNormal.y;
+                currentBlendshapeOffset.normalOffsetZ += blendshapeNormal.z;
                 if (j < blendshape.tangents.size()) {
-                    currentBlendshapeOffset.tangentOffset += blendshape.tangents.at(j) * normalCoefficient;
+                    glm::vec3 blendshapeTangent = blendshape.tangents.at(j) * normalCoefficient;
+                    currentBlendshapeOffset.tangentOffsetX += blendshapeTangent.x;
+                    currentBlendshapeOffset.tangentOffsetY += blendshapeTangent.y;
+                    currentBlendshapeOffset.tangentOffsetZ += blendshapeTangent.z;
                 }
             }
         }
diff --git a/libraries/render-utils/src/RenderCommonTask.cpp b/libraries/render-utils/src/RenderCommonTask.cpp
index bffa6eb366..1055a4256d 100644
--- a/libraries/render-utils/src/RenderCommonTask.cpp
+++ b/libraries/render-utils/src/RenderCommonTask.cpp
@@ -94,6 +94,8 @@ void DrawLayered3D::run(const RenderContextPointer& renderContext, const Inputs&
     }
 
     if (!inItems.empty()) {
+        auto deferredLightingEffect = DependencyManager::get<DeferredLightingEffect>();
+
         // Render the items
         gpu::doInBatch("DrawLayered3D::main", args->_context, [&](gpu::Batch& batch) {
             args->_batch = &batch;
@@ -117,11 +119,20 @@ void DrawLayered3D::run(const RenderContextPointer& renderContext, const Inputs&
                 batch.setUniformBuffer(graphics::slot::buffer::Buffer::HazeParams, haze->getHazeParametersBuffer());
             }
 
+            // Set the light
+            deferredLightingEffect->setupKeyLightBatch(args, batch);
+
+            auto renderMethod = args->_renderMethod;
+            args->_renderMethod = Args::RenderMethod::FORWARD;
             if (_opaquePass) {
                 renderStateSortShapes(renderContext, _shapePlumber, inItems, _maxDrawn);
             } else {
                 renderShapes(renderContext, _shapePlumber, inItems, _maxDrawn);
             }
+
+            deferredLightingEffect->unsetLocalLightsBatch(batch);
+
+            args->_renderMethod = renderMethod;
             args->_batch = nullptr;
         });
     }
diff --git a/libraries/render-utils/src/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp
index b5a88b0ba9..e7253f6adc 100644
--- a/libraries/render-utils/src/RenderPipelines.cpp
+++ b/libraries/render-utils/src/RenderPipelines.cpp
@@ -5,6 +5,7 @@
 //
 //  Created by Zach Pomerantz on 1/28/2016.
 //  Copyright 2016 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -28,6 +29,8 @@
 
 using namespace render;
 using namespace std::placeholders;
+using namespace shader::render_utils::program;
+using Key = render::ShapeKey;
 
 namespace ru {
     using render_utils::slot::texture::Texture;
@@ -43,140 +46,167 @@ void initDeferredPipelines(ShapePlumber& plumber, const render::ShapePipeline::B
 void initForwardPipelines(ShapePlumber& plumber);
 void initZPassPipelines(ShapePlumber& plumber, gpu::StatePointer state, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter);
 void initMirrorPipelines(ShapePlumber& plumber, gpu::StatePointer state, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter, bool forward);
+void sortAndRenderZPassShapes(const ShapePlumberPointer& shapePlumber, const render::RenderContextPointer& renderContext, const render::ShapeBounds& inShapes, render::ItemBounds &itemBounds);
 
 void addPlumberPipeline(ShapePlumber& plumber,
-        const ShapeKey& key, int programId,
+        const ShapeKey& key, int programId, gpu::StatePointer& baseState,
         const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter);
 
 void batchSetter(const ShapePipeline& pipeline, gpu::Batch& batch, RenderArgs* args);
 void lightBatchSetter(const ShapePipeline& pipeline, gpu::Batch& batch, RenderArgs* args);
 static bool forceLightBatchSetter{ false };
 
+// TOOD: build this list algorithmically so we don't have to maintain it
+std::vector<std::tuple<Key::Builder, uint32_t, uint32_t>> ALL_PIPELINES = {
+    // Simple
+    { Key::Builder(), simple, model_shadow },
+    { Key::Builder().withTranslucent(), simple_translucent, model_shadow },
+    { Key::Builder().withUnlit(), simple_unlit, model_shadow },
+    { Key::Builder().withTranslucent().withUnlit(), simple_translucent_unlit, model_shadow },
+    // Simple Fade
+    { Key::Builder().withFade(), simple_fade, model_shadow_fade },
+    { Key::Builder().withTranslucent().withFade(), simple_translucent_fade, model_shadow_fade },
+    { Key::Builder().withUnlit().withFade(), simple_unlit_fade, model_shadow_fade },
+    { Key::Builder().withTranslucent().withUnlit().withFade(), simple_translucent_unlit_fade, model_shadow_fade },
+
+    // Unskinned
+    { Key::Builder().withMaterial(), model, model_shadow },
+    { Key::Builder().withMaterial().withTangents(), model_normalmap, model_shadow },
+    { Key::Builder().withMaterial().withTranslucent(), model_translucent, model_shadow },
+    { Key::Builder().withMaterial().withTangents().withTranslucent(), model_normalmap_translucent, model_shadow },
+    // Unskinned Unlit
+    { Key::Builder().withMaterial().withUnlit(), model_unlit, model_shadow },
+    { Key::Builder().withMaterial().withTangents().withUnlit(), model_normalmap_unlit, model_shadow },
+    { Key::Builder().withMaterial().withTranslucent().withUnlit(), model_translucent_unlit, model_shadow },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withUnlit(), model_normalmap_translucent_unlit, model_shadow },
+    // Unskinned Lightmapped
+    { Key::Builder().withMaterial().withLightMap(), model_lightmap, model_shadow },
+    { Key::Builder().withMaterial().withTangents().withLightMap(), model_normalmap_lightmap, model_shadow },
+    { Key::Builder().withMaterial().withTranslucent().withLightMap(), model_translucent_lightmap, model_shadow },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap(), model_normalmap_translucent_lightmap, model_shadow },
+    // Unskinned MToon
+    { Key::Builder().withMaterial().withMToon(), model_mtoon, model_shadow_mtoon },
+    { Key::Builder().withMaterial().withTangents().withMToon(), model_normalmap_mtoon, model_shadow_mtoon },
+    { Key::Builder().withMaterial().withTranslucent().withMToon(), model_translucent_mtoon, model_shadow_mtoon },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon(), model_normalmap_translucent_mtoon, model_shadow_mtoon },
+    // Unskinned Fade
+    { Key::Builder().withMaterial().withFade(), model_fade, model_shadow_fade },
+    { Key::Builder().withMaterial().withTangents().withFade(), model_normalmap_fade, model_shadow_fade },
+    { Key::Builder().withMaterial().withTranslucent().withFade(), model_translucent_fade, model_shadow_fade },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withFade(), model_normalmap_translucent_fade, model_shadow_fade },
+    // Unskinned Unlit Fade
+    { Key::Builder().withMaterial().withUnlit().withFade(), model_unlit_fade, model_shadow_fade },
+    { Key::Builder().withMaterial().withTangents().withUnlit().withFade(), model_normalmap_unlit_fade, model_shadow_fade },
+    { Key::Builder().withMaterial().withTranslucent().withUnlit().withFade(), model_translucent_unlit_fade, model_shadow_fade },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withUnlit().withFade(), model_normalmap_translucent_unlit_fade, model_shadow_fade },
+    // Unskinned Lightmapped Fade
+    { Key::Builder().withMaterial().withLightMap().withFade(), model_lightmap_fade, model_shadow_fade },
+    { Key::Builder().withMaterial().withTangents().withLightMap().withFade(), model_normalmap_lightmap_fade, model_shadow_fade },
+    { Key::Builder().withMaterial().withTranslucent().withLightMap().withFade(), model_translucent_lightmap_fade, model_shadow_fade },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap().withFade(), model_normalmap_translucent_lightmap_fade, model_shadow_fade },
+    // Unskinned MToon Fade
+    { Key::Builder().withMaterial().withMToon().withFade(), model_mtoon_fade, model_shadow_mtoon_fade },
+    { Key::Builder().withMaterial().withTangents().withMToon().withFade(), model_normalmap_mtoon_fade, model_shadow_mtoon_fade },
+    { Key::Builder().withMaterial().withTranslucent().withMToon().withFade(), model_translucent_mtoon_fade, model_shadow_mtoon_fade },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon().withFade(), model_normalmap_translucent_mtoon_fade, model_shadow_mtoon_fade },
+
+    // Matrix palette skinned
+    { Key::Builder().withMaterial().withDeformed(), model_deformed, model_shadow_deformed },
+    { Key::Builder().withMaterial().withTangents().withDeformed(), model_normalmap_deformed, model_shadow_deformed },
+    { Key::Builder().withMaterial().withTranslucent().withDeformed(), model_translucent_deformed, model_shadow_deformed },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withDeformed(), model_normalmap_translucent_deformed, model_shadow_deformed },
+    // Matrix palette skinned Unlit
+    { Key::Builder().withMaterial().withUnlit().withDeformed(), model_unlit_deformed, model_shadow_deformed },
+    { Key::Builder().withMaterial().withTangents().withUnlit().withDeformed(), model_normalmap_unlit_deformed, model_shadow_deformed },
+    { Key::Builder().withMaterial().withTranslucent().withUnlit().withDeformed(), model_translucent_unlit_deformed, model_shadow_deformed },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withUnlit().withDeformed(), model_normalmap_translucent_unlit_deformed, model_shadow_deformed },
+    // Matrix palette skinned Lightmapped
+    { Key::Builder().withMaterial().withLightMap().withDeformed(), model_lightmap_deformed, model_shadow_deformed },
+    { Key::Builder().withMaterial().withTangents().withLightMap().withDeformed(), model_normalmap_lightmap_deformed, model_shadow_deformed },
+    { Key::Builder().withMaterial().withTranslucent().withLightMap().withDeformed(), model_translucent_lightmap_deformed, model_shadow_deformed },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap().withDeformed(), model_normalmap_translucent_lightmap_deformed, model_shadow_deformed },
+    // Matrix palette skinned MToon
+    { Key::Builder().withMaterial().withMToon().withDeformed(), model_mtoon_deformed, model_shadow_mtoon_deformed },
+    { Key::Builder().withMaterial().withTangents().withMToon().withDeformed(), model_normalmap_mtoon_deformed, model_shadow_mtoon_deformed },
+    { Key::Builder().withMaterial().withTranslucent().withMToon().withDeformed(), model_translucent_mtoon_deformed, model_shadow_mtoon_deformed },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon().withDeformed(), model_normalmap_translucent_mtoon_deformed, model_shadow_mtoon_deformed },
+    // Matrix palette skinned Fade
+    { Key::Builder().withMaterial().withFade().withDeformed(), model_fade_deformed, model_shadow_fade_deformed },
+    { Key::Builder().withMaterial().withTangents().withFade().withDeformed(), model_normalmap_fade_deformed, model_shadow_fade_deformed },
+    { Key::Builder().withMaterial().withTranslucent().withFade().withDeformed(), model_translucent_fade_deformed, model_shadow_fade_deformed },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withFade().withDeformed(), model_normalmap_translucent_fade_deformed, model_shadow_fade_deformed },
+    // Matrix palette skinned Unlit Fade
+    { Key::Builder().withMaterial().withUnlit().withFade().withDeformed(), model_unlit_fade_deformed, model_shadow_fade_deformed },
+    { Key::Builder().withMaterial().withTangents().withUnlit().withFade().withDeformed(), model_normalmap_unlit_fade_deformed, model_shadow_fade_deformed },
+    { Key::Builder().withMaterial().withTranslucent().withUnlit().withFade().withDeformed(), model_translucent_unlit_fade_deformed, model_shadow_fade_deformed },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withUnlit().withFade().withDeformed(), model_normalmap_translucent_unlit_fade_deformed, model_shadow_fade_deformed },
+    // Matrix palette skinned Lightmapped Fade
+    { Key::Builder().withMaterial().withLightMap().withFade().withDeformed(), model_lightmap_fade_deformed, model_shadow_fade_deformed },
+    { Key::Builder().withMaterial().withTangents().withLightMap().withFade().withDeformed(), model_normalmap_lightmap_fade_deformed, model_shadow_fade_deformed },
+    { Key::Builder().withMaterial().withTranslucent().withLightMap().withFade().withDeformed(), model_translucent_lightmap_fade_deformed, model_shadow_fade_deformed },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap().withFade().withDeformed(), model_normalmap_translucent_lightmap_fade_deformed, model_shadow_fade_deformed },
+    // Matrix palette skinned MToon Fade
+    { Key::Builder().withMaterial().withMToon().withFade().withDeformed(), model_mtoon_fade_deformed, model_shadow_mtoon_fade_deformed },
+    { Key::Builder().withMaterial().withTangents().withMToon().withFade().withDeformed(), model_normalmap_mtoon_fade_deformed, model_shadow_mtoon_fade_deformed },
+    { Key::Builder().withMaterial().withTranslucent().withMToon().withFade().withDeformed(), model_translucent_mtoon_fade_deformed, model_shadow_mtoon_fade_deformed },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon().withFade().withDeformed(), model_normalmap_translucent_mtoon_fade_deformed, model_shadow_mtoon_fade_deformed },
+
+    // Dual quaternion skinned
+    { Key::Builder().withMaterial().withDeformed().withDualQuatSkinned(), model_deformeddq, model_shadow_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withDeformed().withDualQuatSkinned(), model_normalmap_deformeddq, model_shadow_deformeddq },
+    { Key::Builder().withMaterial().withTranslucent().withDeformed().withDualQuatSkinned(), model_translucent_deformeddq, model_shadow_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_deformeddq, model_shadow_deformeddq },
+    // Dual quaternion skinned Unlit
+    { Key::Builder().withMaterial().withUnlit().withDeformed().withDualQuatSkinned(), model_unlit_deformeddq, model_shadow_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withUnlit().withDeformed().withDualQuatSkinned(), model_normalmap_unlit_deformeddq, model_shadow_deformeddq },
+    { Key::Builder().withMaterial().withTranslucent().withUnlit().withDeformed().withDualQuatSkinned(), model_translucent_unlit_deformeddq, model_shadow_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withUnlit().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_unlit_deformeddq, model_shadow_deformeddq },
+    // Dual quaternion skinned Lightmapped
+    { Key::Builder().withMaterial().withLightMap().withDeformed().withDualQuatSkinned(), model_lightmap_deformeddq, model_shadow_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withLightMap().withDeformed().withDualQuatSkinned(), model_normalmap_lightmap_deformeddq, model_shadow_deformeddq },
+    { Key::Builder().withMaterial().withTranslucent().withLightMap().withDeformed().withDualQuatSkinned(), model_translucent_lightmap_deformeddq, model_shadow_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_lightmap_deformeddq, model_shadow_deformeddq },
+    // Dual quaternion skinned MToon
+    { Key::Builder().withMaterial().withMToon().withDeformed().withDualQuatSkinned(), model_mtoon_deformeddq, model_shadow_mtoon_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withMToon().withDeformed().withDualQuatSkinned(), model_normalmap_mtoon_deformeddq, model_shadow_mtoon_deformeddq },
+    { Key::Builder().withMaterial().withTranslucent().withMToon().withDeformed().withDualQuatSkinned(), model_translucent_mtoon_deformeddq, model_shadow_mtoon_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_mtoon_deformeddq, model_shadow_mtoon_deformeddq },
+    // Dual quaternion skinned Fade
+    { Key::Builder().withMaterial().withFade().withDeformed().withDualQuatSkinned(), model_fade_deformeddq, model_shadow_fade_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_fade_deformeddq, model_shadow_fade_deformeddq },
+    { Key::Builder().withMaterial().withTranslucent().withFade().withDeformed().withDualQuatSkinned(), model_translucent_fade_deformeddq, model_shadow_fade_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_fade_deformeddq, model_shadow_fade_deformeddq },
+    // Dual quaternion skinned Unlit Fade
+    { Key::Builder().withMaterial().withUnlit().withFade().withDeformed().withDualQuatSkinned(), model_unlit_fade_deformeddq, model_shadow_fade_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withUnlit().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_unlit_fade_deformeddq, model_shadow_fade_deformeddq },
+    { Key::Builder().withMaterial().withTranslucent().withUnlit().withFade().withDeformed().withDualQuatSkinned(), model_translucent_unlit_fade_deformeddq, model_shadow_fade_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withUnlit().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_unlit_fade_deformeddq, model_shadow_fade_deformeddq },
+    // Dual quaternion skinned Lightmapped Fade
+    { Key::Builder().withMaterial().withLightMap().withFade().withDeformed().withDualQuatSkinned(), model_lightmap_fade_deformeddq, model_shadow_fade_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withLightMap().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_lightmap_fade_deformeddq, model_shadow_fade_deformeddq },
+    { Key::Builder().withMaterial().withTranslucent().withLightMap().withFade().withDeformed().withDualQuatSkinned(), model_translucent_lightmap_fade_deformeddq, model_shadow_fade_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_lightmap_fade_deformeddq, model_shadow_fade_deformeddq },
+    // Dual quaternion skinned MToon Fade
+    { Key::Builder().withMaterial().withMToon().withFade().withDeformed().withDualQuatSkinned(), model_mtoon_fade_deformeddq, model_shadow_mtoon_fade_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withMToon().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_mtoon_fade_deformeddq, model_shadow_mtoon_fade_deformeddq },
+    { Key::Builder().withMaterial().withTranslucent().withMToon().withFade().withDeformed().withDualQuatSkinned(), model_translucent_mtoon_fade_deformeddq, model_shadow_mtoon_fade_deformeddq },
+    { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_mtoon_fade_deformeddq, model_shadow_mtoon_fade_deformeddq },
+};
+
 void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter) {
-    using namespace shader::render_utils::program;
-    using Key = render::ShapeKey;
-    auto addPipeline = std::bind(&addPlumberPipeline, std::ref(plumber), _1, _2, _3, _4);
+    auto addPipeline = std::bind(&addPlumberPipeline, std::ref(plumber), _1, _2, std::make_shared<gpu::State>(), _3, _4);
 
-    // TOOD: build this list algorithmically so we don't have to maintain it
-    std::vector<std::pair<render::ShapeKey::Builder, uint32_t>> pipelines = {
-        // Simple
-        { Key::Builder(), simple },
-        { Key::Builder().withTranslucent(), simple_translucent },
-        { Key::Builder().withUnlit(), simple_unlit },
-        { Key::Builder().withTranslucent().withUnlit(), simple_translucent_unlit },
-        // Simple Fade
-        { Key::Builder().withFade(), simple_fade },
-        { Key::Builder().withTranslucent().withFade(), simple_translucent_fade },
-        { Key::Builder().withUnlit().withFade(), simple_unlit_fade },
-        { Key::Builder().withTranslucent().withUnlit().withFade(), simple_translucent_unlit_fade },
-
-        // Unskinned
-        { Key::Builder().withMaterial(), model },
-        { Key::Builder().withMaterial().withTangents(), model_normalmap },
-        { Key::Builder().withMaterial().withTranslucent(), model_translucent },
-        { Key::Builder().withMaterial().withTangents().withTranslucent(), model_normalmap_translucent },
-        // Unskinned Unlit
-        { Key::Builder().withMaterial().withUnlit(), model_unlit },
-        { Key::Builder().withMaterial().withTangents().withUnlit(), model_normalmap_unlit },
-        { Key::Builder().withMaterial().withTranslucent().withUnlit(), model_translucent_unlit },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withUnlit(), model_normalmap_translucent_unlit },
-        // Unskinned Lightmapped
-        { Key::Builder().withMaterial().withLightMap(), model_lightmap },
-        { Key::Builder().withMaterial().withTangents().withLightMap(), model_normalmap_lightmap },
-        { Key::Builder().withMaterial().withTranslucent().withLightMap(), model_translucent_lightmap },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap(), model_normalmap_translucent_lightmap },
-        // Unskinned Fade
-        { Key::Builder().withMaterial().withFade(), model_fade },
-        { Key::Builder().withMaterial().withTangents().withFade(), model_normalmap_fade },
-        { Key::Builder().withMaterial().withTranslucent().withFade(), model_translucent_fade },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withFade(), model_normalmap_translucent_fade },
-        // Unskinned Unlit Fade
-        { Key::Builder().withMaterial().withUnlit().withFade(), model_unlit_fade },
-        { Key::Builder().withMaterial().withTangents().withUnlit().withFade(), model_normalmap_unlit_fade },
-        { Key::Builder().withMaterial().withTranslucent().withUnlit().withFade(), model_translucent_unlit_fade },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withUnlit().withFade(), model_normalmap_translucent_unlit_fade },
-        // Unskinned Lightmapped Fade
-        { Key::Builder().withMaterial().withLightMap().withFade(), model_lightmap_fade },
-        { Key::Builder().withMaterial().withTangents().withLightMap().withFade(), model_normalmap_lightmap_fade },
-        { Key::Builder().withMaterial().withTranslucent().withLightMap().withFade(), model_translucent_lightmap_fade },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap().withFade(), model_normalmap_translucent_lightmap_fade },
-
-        // Matrix palette skinned
-        { Key::Builder().withMaterial().withDeformed(), model_deformed },
-        { Key::Builder().withMaterial().withTangents().withDeformed(), model_normalmap_deformed },
-        { Key::Builder().withMaterial().withTranslucent().withDeformed(), model_translucent_deformed },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withDeformed(), model_normalmap_translucent_deformed },
-        // Matrix palette skinned Unlit
-        { Key::Builder().withMaterial().withUnlit().withDeformed(), model_unlit_deformed },
-        { Key::Builder().withMaterial().withTangents().withUnlit().withDeformed(), model_normalmap_unlit_deformed },
-        { Key::Builder().withMaterial().withTranslucent().withUnlit().withDeformed(), model_translucent_unlit_deformed },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withUnlit().withDeformed(), model_normalmap_translucent_unlit_deformed },
-        // Matrix palette skinned Lightmapped
-        { Key::Builder().withMaterial().withLightMap().withDeformed(), model_lightmap_deformed },
-        { Key::Builder().withMaterial().withTangents().withLightMap().withDeformed(), model_normalmap_lightmap_deformed },
-        { Key::Builder().withMaterial().withTranslucent().withLightMap().withDeformed(), model_translucent_lightmap_deformed },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap().withDeformed(), model_normalmap_translucent_lightmap_deformed },
-        // Matrix palette skinned Fade
-        { Key::Builder().withMaterial().withFade().withDeformed(), model_fade_deformed },
-        { Key::Builder().withMaterial().withTangents().withFade().withDeformed(), model_normalmap_fade_deformed },
-        { Key::Builder().withMaterial().withTranslucent().withFade().withDeformed(), model_translucent_fade_deformed },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withFade().withDeformed(), model_normalmap_translucent_fade_deformed },
-        // Matrix palette skinned Unlit Fade
-        { Key::Builder().withMaterial().withUnlit().withFade().withDeformed(), model_unlit_fade_deformed },
-        { Key::Builder().withMaterial().withTangents().withUnlit().withFade().withDeformed(), model_normalmap_unlit_fade_deformed },
-        { Key::Builder().withMaterial().withTranslucent().withUnlit().withFade().withDeformed(), model_translucent_unlit_fade_deformed },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withUnlit().withFade().withDeformed(), model_normalmap_translucent_unlit_fade_deformed },
-        // Matrix palette skinned Lightmapped Fade
-        { Key::Builder().withMaterial().withLightMap().withFade().withDeformed(), model_lightmap_fade_deformed },
-        { Key::Builder().withMaterial().withTangents().withLightMap().withFade().withDeformed(), model_normalmap_lightmap_fade_deformed },
-        { Key::Builder().withMaterial().withTranslucent().withLightMap().withFade().withDeformed(), model_translucent_lightmap_fade_deformed },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap().withFade().withDeformed(), model_normalmap_translucent_lightmap_fade_deformed },
-
-        // Dual quaternion skinned
-        { Key::Builder().withMaterial().withDeformed().withDualQuatSkinned(), model_deformeddq },
-        { Key::Builder().withMaterial().withTangents().withDeformed().withDualQuatSkinned(), model_normalmap_deformeddq },
-        { Key::Builder().withMaterial().withTranslucent().withDeformed().withDualQuatSkinned(), model_translucent_deformeddq },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_deformeddq },
-        // Dual quaternion skinned Unlit
-        { Key::Builder().withMaterial().withUnlit().withDeformed().withDualQuatSkinned(), model_unlit_deformeddq },
-        { Key::Builder().withMaterial().withTangents().withUnlit().withDeformed().withDualQuatSkinned(), model_normalmap_unlit_deformeddq },
-        { Key::Builder().withMaterial().withTranslucent().withUnlit().withDeformed().withDualQuatSkinned(), model_translucent_unlit_deformeddq },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withUnlit().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_unlit_deformeddq },
-        // Dual quaternion skinned Lightmapped
-        { Key::Builder().withMaterial().withLightMap().withDeformed().withDualQuatSkinned(), model_lightmap_deformeddq },
-        { Key::Builder().withMaterial().withTangents().withLightMap().withDeformed().withDualQuatSkinned(), model_normalmap_lightmap_deformeddq },
-        { Key::Builder().withMaterial().withTranslucent().withLightMap().withDeformed().withDualQuatSkinned(), model_translucent_lightmap_deformeddq },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_lightmap_deformeddq },
-        // Dual quaternion skinned Fade
-        { Key::Builder().withMaterial().withFade().withDeformed().withDualQuatSkinned(), model_fade_deformeddq },
-        { Key::Builder().withMaterial().withTangents().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_fade_deformeddq },
-        { Key::Builder().withMaterial().withTranslucent().withFade().withDeformed().withDualQuatSkinned(), model_translucent_fade_deformeddq },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_fade_deformeddq },
-        // Dual quaternion skinned Unlit Fade
-        { Key::Builder().withMaterial().withUnlit().withFade().withDeformed().withDualQuatSkinned(), model_unlit_fade_deformeddq },
-        { Key::Builder().withMaterial().withTangents().withUnlit().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_unlit_fade_deformeddq },
-        { Key::Builder().withMaterial().withTranslucent().withUnlit().withFade().withDeformed().withDualQuatSkinned(), model_translucent_unlit_fade_deformeddq },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withUnlit().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_unlit_fade_deformeddq },
-        // Dual quaternion skinned Lightmapped Fade
-        { Key::Builder().withMaterial().withLightMap().withFade().withDeformed().withDualQuatSkinned(), model_lightmap_fade_deformeddq },
-        { Key::Builder().withMaterial().withTangents().withLightMap().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_lightmap_fade_deformeddq },
-        { Key::Builder().withMaterial().withTranslucent().withLightMap().withFade().withDeformed().withDualQuatSkinned(), model_translucent_lightmap_fade_deformeddq },
-        { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_lightmap_fade_deformeddq },
-    };
-
-    for (auto& pipeline : pipelines) {
-        if (pipeline.first.build().isFaded()) {
-            addPipeline(pipeline.first, pipeline.second, batchSetter, itemSetter);
+    for (auto& pipeline : ALL_PIPELINES) {
+        if (std::get<0>(pipeline).build().isFaded()) {
+            addPipeline(std::get<0>(pipeline), std::get<1>(pipeline), batchSetter, itemSetter);
         } else {
-            addPipeline(pipeline.first, pipeline.second, nullptr, nullptr);
+            addPipeline(std::get<0>(pipeline), std::get<1>(pipeline), nullptr, nullptr);
         }
     }
 }
 
 void initForwardPipelines(ShapePlumber& plumber) {
-    using namespace shader::render_utils::program;
-    using Key = render::ShapeKey;
-    auto addPipelineBind = std::bind(&addPlumberPipeline, std::ref(plumber), _1, _2, _3, _4);
+    auto addPipelineBind = std::bind(&addPlumberPipeline, std::ref(plumber), _1, _2, std::make_shared<gpu::State>(), _3, _4);
 
     // Disable fade on the forward pipeline, all shaders get added twice, once with the fade key and once without
     auto addPipeline = [&](const ShapeKey& key, int programId) {
@@ -188,7 +218,7 @@ void initForwardPipelines(ShapePlumber& plumber) {
     forceLightBatchSetter = true;
 
     // TOOD: build this list algorithmically so we don't have to maintain it
-    std::vector<std::pair<render::ShapeKey::Builder, uint32_t>> pipelines = {
+    std::vector<std::pair<Key::Builder, uint32_t>> pipelines = {
         // Simple
         { Key::Builder(), simple_forward },
         { Key::Builder().withTranslucent(), simple_translucent_forward },
@@ -210,6 +240,11 @@ void initForwardPipelines(ShapePlumber& plumber) {
         { Key::Builder().withMaterial().withTangents().withLightMap(), model_normalmap_lightmap_forward },
         { Key::Builder().withMaterial().withTranslucent().withLightMap(), model_translucent_lightmap_forward },
         { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap(), model_normalmap_translucent_lightmap_forward },
+        // Unskinned MToon
+        { Key::Builder().withMaterial().withMToon(), model_mtoon_forward },
+        { Key::Builder().withMaterial().withTangents().withMToon(), model_normalmap_mtoon_forward },
+        { Key::Builder().withMaterial().withTranslucent().withMToon(), model_translucent_mtoon_forward },
+        { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon(), model_normalmap_translucent_mtoon_forward },
 
         // Matrix palette skinned
         { Key::Builder().withMaterial().withDeformed(), model_forward_deformed },
@@ -226,6 +261,11 @@ void initForwardPipelines(ShapePlumber& plumber) {
         { Key::Builder().withMaterial().withTangents().withLightMap().withDeformed(), model_normalmap_lightmap_forward_deformed },
         { Key::Builder().withMaterial().withTranslucent().withLightMap().withDeformed(), model_translucent_lightmap_forward_deformed },
         { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap().withDeformed(), model_normalmap_translucent_lightmap_forward_deformed },
+        // Matrix palette skinned MToon
+        { Key::Builder().withMaterial().withMToon().withDeformed(), model_mtoon_forward_deformed },
+        { Key::Builder().withMaterial().withTangents().withMToon().withDeformed(), model_normalmap_mtoon_forward_deformed },
+        { Key::Builder().withMaterial().withTranslucent().withMToon().withDeformed(), model_translucent_mtoon_forward_deformed },
+        { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon().withDeformed(), model_normalmap_translucent_mtoon_forward_deformed },
 
         // Dual quaternion skinned
         { Key::Builder().withMaterial().withDeformed().withDualQuatSkinned(), model_forward_deformeddq },
@@ -242,6 +282,11 @@ void initForwardPipelines(ShapePlumber& plumber) {
         { Key::Builder().withMaterial().withTangents().withLightMap().withDeformed().withDualQuatSkinned(), model_normalmap_lightmap_forward_deformeddq },
         { Key::Builder().withMaterial().withTranslucent().withLightMap().withDeformed().withDualQuatSkinned(), model_translucent_lightmap_forward_deformeddq },
         { Key::Builder().withMaterial().withTangents().withTranslucent().withLightMap().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_lightmap_forward_deformeddq },
+        // Dual quaternion skinned MToon
+        { Key::Builder().withMaterial().withMToon().withDeformed().withDualQuatSkinned(), model_mtoon_forward_deformeddq },
+        { Key::Builder().withMaterial().withTangents().withMToon().withDeformed().withDualQuatSkinned(), model_normalmap_mtoon_forward_deformeddq },
+        { Key::Builder().withMaterial().withTranslucent().withMToon().withDeformed().withDualQuatSkinned(), model_translucent_mtoon_forward_deformeddq },
+        { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_mtoon_forward_deformeddq },
     };
 
     for (auto& pipeline : pipelines) {
@@ -252,7 +297,7 @@ void initForwardPipelines(ShapePlumber& plumber) {
 }
 
 void addPlumberPipeline(ShapePlumber& plumber,
-        const ShapeKey& key, int programId,
+        const ShapeKey& key, int programId, gpu::StatePointer& baseState,
         const render::ShapePipeline::BatchSetter& extraBatchSetter, const render::ShapePipeline::ItemSetter& itemSetter) {
     // These key-values' pipelines are added by this functor in addition to the key passed
     assert(!key.isWireframe());
@@ -265,7 +310,7 @@ void addPlumberPipeline(ShapePlumber& plumber,
         bool isBiased = (i & 1);
         bool isWireframed = (i & 2);
         for (int cullFaceMode = graphics::MaterialKey::CullFaceMode::CULL_NONE; cullFaceMode < graphics::MaterialKey::CullFaceMode::NUM_CULL_FACE_MODES; cullFaceMode++) {
-            auto state = std::make_shared<gpu::State>();
+            auto state = std::make_shared<gpu::State>(*baseState);
             key.isTranslucent() ? PrepareStencil::testMask(*state) : PrepareStencil::testMaskDrawShape(*state);
 
             // Depth test depends on transparency
@@ -287,7 +332,7 @@ void addPlumberPipeline(ShapePlumber& plumber,
                 state->setDepthBiasSlopeScale(1.0f);
             }
 
-            auto baseBatchSetter = (forceLightBatchSetter || key.isTranslucent()) ? &lightBatchSetter : &batchSetter;
+            auto baseBatchSetter = (forceLightBatchSetter || key.isTranslucent() || key.isMToon()) ? &lightBatchSetter : &batchSetter;
             render::ShapePipeline::BatchSetter finalBatchSetter;
             if (extraBatchSetter) {
                 finalBatchSetter = [baseBatchSetter, extraBatchSetter](const ShapePipeline& pipeline, gpu::Batch& batch, render::Args* args) {
@@ -344,29 +389,97 @@ void lightBatchSetter(const ShapePipeline& pipeline, gpu::Batch& batch, RenderAr
     }
 }
 
-void initZPassPipelines(ShapePlumber& shapePlumber, gpu::StatePointer state, const render::ShapePipeline::BatchSetter& extraBatchSetter, const render::ShapePipeline::ItemSetter& itemSetter) {
-    using namespace shader::render_utils::program;
+void initZPassPipelines(ShapePlumber& plumber, gpu::StatePointer state, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter) {
+    auto addPipeline = std::bind(&addPlumberPipeline, std::ref(plumber), _1, _2, state, _3, _4);
 
-    shapePlumber.addPipeline(
-        ShapeKey::Filter::Builder().withoutDeformed().withoutFade(),
-        gpu::Shader::createProgram(model_shadow), state);
-    shapePlumber.addPipeline(
-        ShapeKey::Filter::Builder().withoutDeformed().withFade(),
-        gpu::Shader::createProgram(model_shadow_fade), state, extraBatchSetter, itemSetter);
+    for (auto& pipeline : ALL_PIPELINES) {
+        if (std::get<0>(pipeline).build().isFaded()) {
+            addPipeline(std::get<0>(pipeline), std::get<2>(pipeline), batchSetter, itemSetter);
+        } else {
+            addPipeline(std::get<0>(pipeline), std::get<2>(pipeline), nullptr, nullptr);
+        }
+    }
+}
 
-    shapePlumber.addPipeline(
-        ShapeKey::Filter::Builder().withDeformed().withoutDualQuatSkinned().withoutFade(),
-        gpu::Shader::createProgram(model_shadow_deformed), state);
-    shapePlumber.addPipeline(
-        ShapeKey::Filter::Builder().withDeformed().withoutDualQuatSkinned().withFade(),
-        gpu::Shader::createProgram(model_shadow_fade_deformed), state, extraBatchSetter, itemSetter);
 
-    shapePlumber.addPipeline(
-        ShapeKey::Filter::Builder().withDeformed().withDualQuatSkinned().withoutFade(),
-        gpu::Shader::createProgram(model_shadow_deformeddq), state);
-    shapePlumber.addPipeline(
-        ShapeKey::Filter::Builder().withDeformed().withDualQuatSkinned().withFade(),
-        gpu::Shader::createProgram(model_shadow_fade_deformeddq), state, extraBatchSetter, itemSetter);
+void sortAndRenderZPassShapes(const ShapePlumberPointer& shapePlumber, const render::RenderContextPointer& renderContext, const render::ShapeBounds& inShapes, render::ItemBounds &itemBounds) {
+    std::unordered_map<ShapeKey, std::vector<ShapeKey>, ShapeKey::Hash, ShapeKey::KeyEqual> sortedShapeKeys;
+    std::unordered_map<uint8_t, std::unordered_map<ShapeKey, std::vector<ShapeKey>, ShapeKey::Hash, ShapeKey::KeyEqual>> sortedCustomShapeKeys;
+    std::unordered_map<ShapeKey, std::vector<ShapeKey>, ShapeKey::Hash, ShapeKey::KeyEqual> sortedOwnPipelineShapeKeys;
+
+    for (const auto& items : inShapes) {
+        itemBounds.insert(itemBounds.end(), items.second.begin(), items.second.end());
+
+        ShapeKey::Builder variantKey = ShapeKey::Builder();
+
+        // The keys we need to check here have to match the ones set up in initZPassPipelines (+ addPlumberPipeline)
+        if (items.first.isDeformed()) {
+            variantKey.withDeformed();
+            if (items.first.isDualQuatSkinned()) {
+                variantKey.withDualQuatSkinned();
+            }
+        }
+
+        if (items.first.isFaded()) {
+            variantKey.withFade();
+        }
+
+        if (items.first.isMToon()) {
+            variantKey.withMToon();
+        }
+
+        if (items.first.isCullFace()) {
+            variantKey.withCullFaceMode(graphics::MaterialKey::CULL_BACK);
+        } else if (items.first.isCullFaceFront()) {
+            variantKey.withCullFaceMode(graphics::MaterialKey::CULL_FRONT);
+        } else if (items.first.isCullFaceNone()) {
+            variantKey.withCullFaceMode(graphics::MaterialKey::CULL_NONE);
+        }
+
+        if (items.first.isWireframe()) {
+            variantKey.withWireframe();
+        }
+
+        if (items.first.isDepthBiased()) {
+            variantKey.withDepthBias();
+        }
+
+        if (items.first.hasOwnPipeline()) {
+            sortedOwnPipelineShapeKeys[variantKey.build()].push_back(items.first);
+        } else if (items.first.isCustom()) {
+            const uint8_t custom = items.first.getCustom();
+            variantKey.withCustom(custom);
+            sortedCustomShapeKeys[custom][variantKey.build()].push_back(items.first);
+        } else {
+            sortedShapeKeys[variantKey.build()].push_back(items.first);
+        }
+    }
+
+    // Render non-withCustom, non-withOwnPipeline things
+    for (const auto& variantAndKeys : sortedShapeKeys) {
+        for (const auto& key : variantAndKeys.second) {
+            renderShapes(renderContext, shapePlumber, inShapes.at(key));
+        }
+    }
+
+    // Render withCustom things
+    for (const auto& customAndSortedCustomKeys : sortedCustomShapeKeys) {
+        for (const auto& variantAndKeys : customAndSortedCustomKeys.second) {
+            for (const auto& key : variantAndKeys.second) {
+                renderShapes(renderContext, shapePlumber, inShapes.at(key));
+            }
+        }
+    }
+
+    // Render withOwnPipeline things
+    for (const auto& variantAndKeys : sortedOwnPipelineShapeKeys) {
+        for (const auto& key : variantAndKeys.second) {
+            renderShapes(renderContext, shapePlumber, inShapes.at(key));
+        }
+    }
+
+    renderContext->args->_shapePipeline = nullptr;
+    renderContext->args->_batch = nullptr;
 }
 
 void initMirrorPipelines(ShapePlumber& shapePlumber, gpu::StatePointer state, const render::ShapePipeline::BatchSetter& extraBatchSetter, const render::ShapePipeline::ItemSetter& itemSetter, bool forward) {
@@ -415,11 +528,11 @@ bool RenderPipelines::bindMaterial(graphics::MaterialPointer& material, gpu::Bat
 }
 
 void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial) {
-    auto& schemaBuffer = multiMaterial.getSchemaBuffer();
-
     auto& drawMaterialTextures = multiMaterial.getTextureTable();
     multiMaterial.setTexturesLoading(false);
     multiMaterial.resetReferenceTexturesAndMaterials();
+    multiMaterial.setisMToon(!multiMaterial.empty() && multiMaterial.top().material && multiMaterial.top().material->isMToon());
+    multiMaterial.resetOutline();
 
     // The total list of things we need to look for
     static std::set<uint> allFlags;
@@ -438,6 +551,7 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial
 
     graphics::MultiMaterial materials = multiMaterial;
     graphics::MultiMaterial::Schema schema;
+    graphics::MultiMaterial::MToonSchema toonSchema;
     graphics::MaterialKey schemaKey;
 
     std::set<uint> flagsToCheck = allFlags;
@@ -465,261 +579,570 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial
 
             bool wasSet = false;
             bool forceDefault = false;
-            switch (flag) {
-                case graphics::MaterialKey::EMISSIVE_VAL_BIT:
-                    if (materialKey.isEmissive()) {
-                        schema._emissive = material->getEmissive(false);
-                        schemaKey.setEmissive(true);
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::MaterialKey::UNLIT_VAL_BIT:
-                    if (materialKey.isUnlit()) {
-                        schemaKey.setUnlit(true);
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::MaterialKey::ALBEDO_VAL_BIT:
-                    if (materialKey.isAlbedo()) {
-                        schema._albedo = material->getAlbedo(false);
-                        schemaKey.setAlbedo(true);
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::MaterialKey::METALLIC_VAL_BIT:
-                    if (materialKey.isMetallic()) {
-                        schema._metallic = material->getMetallic();
-                        schemaKey.setMetallic(true);
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::MaterialKey::GLOSSY_VAL_BIT:
-                    if (materialKey.isRough() || materialKey.isGlossy()) {
-                        schema._roughness = material->getRoughness();
-                        schemaKey.setGlossy(materialKey.isGlossy());
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::MaterialKey::OPACITY_VAL_BIT:
-                    if (materialKey.isTranslucentFactor()) {
-                        schema._opacity = material->getOpacity();
-                        schemaKey.setTranslucentFactor(true);
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::MaterialKey::OPACITY_CUTOFF_VAL_BIT:
-                    if (materialKey.isOpacityCutoff()) {
-                        schema._opacityCutoff = material->getOpacityCutoff();
-                        schemaKey.setOpacityCutoff(true);
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::MaterialKey::SCATTERING_VAL_BIT:
-                    if (materialKey.isScattering()) {
-                        schema._scattering = material->getScattering();
-                        schemaKey.setScattering(true);
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::MaterialKey::ALBEDO_MAP_BIT:
-                    if (materialKey.isAlbedoMap()) {
-                        auto itr = textureMaps.find(graphics::MaterialKey::ALBEDO_MAP);
-                        if (itr != textureMaps.end()) {
-                            if (itr->second->isDefined()) {
-                                material->resetOpacityMap();
-                                drawMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, itr->second->getTextureView());
-                                if (itr->second->getTextureView().isReference()) {
-                                    multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+            if (!multiMaterial.isMToon()) {
+                switch (flag) {
+                    case graphics::MaterialKey::EMISSIVE_VAL_BIT:
+                        if (materialKey.isEmissive()) {
+                            schema._emissive = material->getEmissive(false);
+                            schemaKey.setEmissive(true);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::MaterialKey::UNLIT_VAL_BIT:
+                        if (materialKey.isUnlit()) {
+                            schemaKey.setUnlit(true);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::MaterialKey::ALBEDO_VAL_BIT:
+                        if (materialKey.isAlbedo()) {
+                            schema._albedo = material->getAlbedo(false);
+                            schemaKey.setAlbedo(true);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::MaterialKey::METALLIC_VAL_BIT:
+                        if (materialKey.isMetallic()) {
+                            schema._metallic = material->getMetallic();
+                            schemaKey.setMetallic(true);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::MaterialKey::GLOSSY_VAL_BIT:
+                        if (materialKey.isRough() || materialKey.isGlossy()) {
+                            schema._roughness = material->getRoughness();
+                            schemaKey.setGlossy(materialKey.isGlossy());
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::MaterialKey::OPACITY_VAL_BIT:
+                        if (materialKey.isTranslucentFactor()) {
+                            schema._opacity = material->getOpacity();
+                            schemaKey.setTranslucentFactor(true);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::MaterialKey::OPACITY_CUTOFF_VAL_BIT:
+                        if (materialKey.isOpacityCutoff()) {
+                            schema._opacityCutoff = material->getOpacityCutoff();
+                            schemaKey.setOpacityCutoff(true);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::MaterialKey::SCATTERING_VAL_BIT:
+                        if (materialKey.isScattering()) {
+                            schema._scattering = material->getScattering();
+                            schemaKey.setScattering(true);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::MaterialKey::ALBEDO_MAP_BIT:
+                        if (materialKey.isAlbedoMap()) {
+                            auto itr = textureMaps.find(graphics::MaterialKey::ALBEDO_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    material->resetOpacityMap();
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
                                 }
-                                wasSet = true;
                             } else {
-                                multiMaterial.setTexturesLoading(true);
                                 forceDefault = true;
                             }
-                        } else {
-                            forceDefault = true;
+                            schemaKey.setAlbedoMap(true);
+                            schemaKey.setOpacityMaskMap(material->getKey().isOpacityMaskMap());
+                            schemaKey.setTranslucentMap(material->getKey().isTranslucentMap());
                         }
-                        schemaKey.setAlbedoMap(true);
-                        schemaKey.setOpacityMaskMap(material->getKey().isOpacityMaskMap());
-                        schemaKey.setTranslucentMap(material->getKey().isTranslucentMap());
-                    }
-                    break;
-                case graphics::MaterialKey::METALLIC_MAP_BIT:
-                    if (materialKey.isMetallicMap()) {
-                        auto itr = textureMaps.find(graphics::MaterialKey::METALLIC_MAP);
-                        if (itr != textureMaps.end()) {
-                            if (itr->second->isDefined()) {
-                                drawMaterialTextures->setTexture(gr::Texture::MaterialMetallic, itr->second->getTextureView());
-                                if (itr->second->getTextureView().isReference()) {
-                                    multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                        break;
+                    case graphics::MaterialKey::METALLIC_MAP_BIT:
+                        if (materialKey.isMetallicMap()) {
+                            auto itr = textureMaps.find(graphics::MaterialKey::METALLIC_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialMetallic, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
                                 }
-                                wasSet = true;
                             } else {
-                                multiMaterial.setTexturesLoading(true);
                                 forceDefault = true;
                             }
-                        } else {
-                            forceDefault = true;
+                            schemaKey.setMetallicMap(true);
                         }
-                        schemaKey.setMetallicMap(true);
-                    }
-                    break;
-                case graphics::MaterialKey::ROUGHNESS_MAP_BIT:
-                    if (materialKey.isRoughnessMap()) {
-                        auto itr = textureMaps.find(graphics::MaterialKey::ROUGHNESS_MAP);
-                        if (itr != textureMaps.end()) {
-                            if (itr->second->isDefined()) {
-                                drawMaterialTextures->setTexture(gr::Texture::MaterialRoughness, itr->second->getTextureView());
-                                if (itr->second->getTextureView().isReference()) {
-                                    multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                        break;
+                    case graphics::MaterialKey::ROUGHNESS_MAP_BIT:
+                        if (materialKey.isRoughnessMap()) {
+                            auto itr = textureMaps.find(graphics::MaterialKey::ROUGHNESS_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialRoughness, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
                                 }
-                                wasSet = true;
                             } else {
-                                multiMaterial.setTexturesLoading(true);
                                 forceDefault = true;
                             }
-                        } else {
-                            forceDefault = true;
+                            schemaKey.setRoughnessMap(true);
                         }
-                        schemaKey.setRoughnessMap(true);
-                    }
-                    break;
-                case graphics::MaterialKey::NORMAL_MAP_BIT:
-                    if (materialKey.isNormalMap()) {
-                        auto itr = textureMaps.find(graphics::MaterialKey::NORMAL_MAP);
-                        if (itr != textureMaps.end()) {
-                            if (itr->second->isDefined()) {
-                                drawMaterialTextures->setTexture(gr::Texture::MaterialNormal, itr->second->getTextureView());
-                                if (itr->second->getTextureView().isReference()) {
-                                    multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                        break;
+                    case graphics::MaterialKey::NORMAL_MAP_BIT:
+                        if (materialKey.isNormalMap()) {
+                            auto itr = textureMaps.find(graphics::MaterialKey::NORMAL_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialNormal, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
                                 }
-                                wasSet = true;
                             } else {
-                                multiMaterial.setTexturesLoading(true);
                                 forceDefault = true;
                             }
-                        } else {
-                            forceDefault = true;
+                            schemaKey.setNormalMap(true);
                         }
-                        schemaKey.setNormalMap(true);
-                    }
-                    break;
-                case graphics::MaterialKey::OCCLUSION_MAP_BIT:
-                    if (materialKey.isOcclusionMap()) {
-                        auto itr = textureMaps.find(graphics::MaterialKey::OCCLUSION_MAP);
-                        if (itr != textureMaps.end()) {
-                            if (itr->second->isDefined()) {
-                                drawMaterialTextures->setTexture(gr::Texture::MaterialOcclusion, itr->second->getTextureView());
-                                if (itr->second->getTextureView().isReference()) {
-                                    multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                        break;
+                    case graphics::MaterialKey::OCCLUSION_MAP_BIT:
+                        if (materialKey.isOcclusionMap()) {
+                            auto itr = textureMaps.find(graphics::MaterialKey::OCCLUSION_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialOcclusion, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
                                 }
-                                wasSet = true;
                             } else {
-                                multiMaterial.setTexturesLoading(true);
                                 forceDefault = true;
                             }
-                        } else {
-                            forceDefault = true;
+                            schemaKey.setOcclusionMap(true);
                         }
-                        schemaKey.setOcclusionMap(true);
-                    }
-                    break;
-                case graphics::MaterialKey::SCATTERING_MAP_BIT:
-                    if (materialKey.isScatteringMap()) {
-                        auto itr = textureMaps.find(graphics::MaterialKey::SCATTERING_MAP);
-                        if (itr != textureMaps.end()) {
-                            if (itr->second->isDefined()) {
-                                drawMaterialTextures->setTexture(gr::Texture::MaterialScattering, itr->second->getTextureView());
-                                if (itr->second->getTextureView().isReference()) {
-                                    multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                        break;
+                    case graphics::MaterialKey::SCATTERING_MAP_BIT:
+                        if (materialKey.isScatteringMap()) {
+                            auto itr = textureMaps.find(graphics::MaterialKey::SCATTERING_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialScattering, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
                                 }
-                                wasSet = true;
                             } else {
-                                multiMaterial.setTexturesLoading(true);
                                 forceDefault = true;
                             }
-                        } else {
-                            forceDefault = true;
+                            schemaKey.setScatteringMap(true);
                         }
-                        schemaKey.setScatteringMap(true);
-                    }
-                    break;
-                case graphics::MaterialKey::EMISSIVE_MAP_BIT:
-                    // Lightmap takes precendence over emissive map for legacy reasons
-                    if (materialKey.isEmissiveMap() && !materialKey.isLightMap()) {
-                        auto itr = textureMaps.find(graphics::MaterialKey::EMISSIVE_MAP);
-                        if (itr != textureMaps.end()) {
-                            if (itr->second->isDefined()) {
-                                drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, itr->second->getTextureView());
-                                if (itr->second->getTextureView().isReference()) {
-                                    multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                        break;
+                    case graphics::MaterialKey::EMISSIVE_MAP_BIT:
+                        // Lightmap takes precendence over emissive map for legacy reasons
+                        if (materialKey.isEmissiveMap() && !materialKey.isLightMap()) {
+                            auto itr = textureMaps.find(graphics::MaterialKey::EMISSIVE_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
                                 }
-                                wasSet = true;
                             } else {
-                                multiMaterial.setTexturesLoading(true);
                                 forceDefault = true;
                             }
-                        } else {
-                            forceDefault = true;
+                            schemaKey.setEmissiveMap(true);
+                        } else if (materialKey.isLightMap()) {
+                            // We'll set this later when we check the lightmap
+                            wasSet = true;
                         }
-                        schemaKey.setEmissiveMap(true);
-                    } else if (materialKey.isLightMap()) {
-                        // We'll set this later when we check the lightmap
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::MaterialKey::LIGHT_MAP_BIT:
-                    if (materialKey.isLightMap()) {
-                        auto itr = textureMaps.find(graphics::MaterialKey::LIGHT_MAP);
-                        if (itr != textureMaps.end()) {
-                            if (itr->second->isDefined()) {
-                                drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, itr->second->getTextureView());
-                                if (itr->second->getTextureView().isReference()) {
-                                    multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                        break;
+                    case graphics::MaterialKey::LIGHT_MAP_BIT:
+                        if (materialKey.isLightMap()) {
+                            auto itr = textureMaps.find(graphics::MaterialKey::LIGHT_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
                                 }
-                                wasSet = true;
                             } else {
-                                multiMaterial.setTexturesLoading(true);
                                 forceDefault = true;
                             }
-                        } else {
-                            forceDefault = true;
+                            schemaKey.setLightMap(true);
                         }
-                        schemaKey.setLightMap(true);
-                    }
-                    break;
-                case graphics::Material::TEXCOORDTRANSFORM0:
-                    if (!fallthrough) {
-                        schema._texcoordTransforms[0] = material->getTexCoordTransform(0);
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::Material::TEXCOORDTRANSFORM1:
-                    if (!fallthrough) {
-                        schema._texcoordTransforms[1] = material->getTexCoordTransform(1);
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::Material::LIGHTMAP_PARAMS:
-                    if (!fallthrough) {
-                        schema._lightmapParams = material->getLightmapParams();
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::Material::MATERIAL_PARAMS:
-                    if (!fallthrough) {
-                        schema._materialParams = material->getMaterialParams();
-                        wasSet = true;
-                    }
-                    break;
-                case graphics::Material::CULL_FACE_MODE:
-                    if (!fallthrough) {
-                        multiMaterial.setCullFaceMode(material->getCullFaceMode());
-                        wasSet = true;
-                    }
-                    break;
-                default:
-                    break;
+                        break;
+                    case graphics::Material::TEXCOORDTRANSFORM0:
+                        if (!fallthrough) {
+                            schema._texcoordTransforms[0] = material->getTexCoordTransform(0);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::Material::TEXCOORDTRANSFORM1:
+                        if (!fallthrough) {
+                            schema._texcoordTransforms[1] = material->getTexCoordTransform(1);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::Material::LIGHTMAP_PARAMS:
+                        if (!fallthrough) {
+                            schema._lightmapParams = material->getLightmapParams();
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::Material::MATERIAL_PARAMS:
+                        if (!fallthrough) {
+                            schema._materialParams = material->getMaterialParams();
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::Material::CULL_FACE_MODE:
+                        if (!fallthrough) {
+                            multiMaterial.setCullFaceMode(material->getCullFaceMode());
+                            wasSet = true;
+                        }
+                        break;
+                    default:
+                        break;
+                }
+            } else {
+                switch (flag) {
+                    case graphics::MaterialKey::EMISSIVE_VAL_BIT:
+                        if (materialKey.isEmissive()) {
+                            toonSchema._emissive = material->getEmissive(false);
+                            schemaKey.setEmissive(true);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::MaterialKey::ALBEDO_VAL_BIT:
+                        if (materialKey.isAlbedo()) {
+                            toonSchema._albedo = material->getAlbedo(false);
+                            schemaKey.setAlbedo(true);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::MaterialKey::OPACITY_VAL_BIT:
+                        if (materialKey.isTranslucentFactor()) {
+                            toonSchema._opacity = material->getOpacity();
+                            schemaKey.setTranslucentFactor(true);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::MaterialKey::OPACITY_CUTOFF_VAL_BIT:
+                        if (materialKey.isOpacityCutoff()) {
+                            toonSchema._opacityCutoff = material->getOpacityCutoff();
+                            schemaKey.setOpacityCutoff(true);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::MaterialKey::ALBEDO_MAP_BIT:
+                        if (materialKey.isAlbedoMap()) {
+                            auto itr = textureMaps.find(graphics::MaterialKey::ALBEDO_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    material->resetOpacityMap();
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
+                                }
+                            } else {
+                                forceDefault = true;
+                            }
+                            schemaKey.setAlbedoMap(true);
+                            schemaKey.setOpacityMaskMap(material->getKey().isOpacityMaskMap());
+                            schemaKey.setTranslucentMap(material->getKey().isTranslucentMap());
+                        }
+                        break;
+                    case graphics::MaterialKey::NORMAL_MAP_BIT:
+                        if (materialKey.isNormalMap()) {
+                            auto itr = textureMaps.find(graphics::MaterialKey::NORMAL_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialNormal, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
+                                }
+                            } else {
+                                forceDefault = true;
+                            }
+                            schemaKey.setNormalMap(true);
+                        }
+                        break;
+                    case graphics::MaterialKey::EMISSIVE_MAP_BIT:
+                        // Lightmap takes precendence over emissive map for legacy reasons
+                        if (materialKey.isEmissiveMap() && !materialKey.isLightMap()) {
+                            auto itr = textureMaps.find(graphics::MaterialKey::EMISSIVE_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
+                                }
+                            } else {
+                                forceDefault = true;
+                            }
+                            schemaKey.setEmissiveMap(true);
+                        } else if (materialKey.isLightMap()) {
+                            // We'll set this later when we check the lightmap
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::Material::TEXCOORDTRANSFORM0:
+                        if (!fallthrough) {
+                            toonSchema._texcoordTransforms[0] = material->getTexCoordTransform(0);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::Material::TEXCOORDTRANSFORM1:
+                        if (!fallthrough) {
+                            toonSchema._texcoordTransforms[1] = material->getTexCoordTransform(1);
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::Material::MATERIAL_PARAMS:
+                        if (!fallthrough) {
+                            toonSchema._materialParams = material->getMaterialParams();
+                            wasSet = true;
+                        }
+                        break;
+                    case graphics::Material::CULL_FACE_MODE:
+                        if (!fallthrough) {
+                            multiMaterial.setCullFaceMode(material->getCullFaceMode());
+                            wasSet = true;
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::SHADE_VAL_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::SHADE_VAL_BIT]) {
+                            toonSchema._shade = material->getShade();
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::SHADE_VAL_BIT, true);
+                            wasSet = true;
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::SHADING_SHIFT_VAL_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::SHADING_SHIFT_VAL_BIT]) {
+                            toonSchema._shadingShift = material->getShadingShift();
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::SHADING_SHIFT_VAL_BIT, true);
+                            wasSet = true;
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::SHADING_TOONY_VAL_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::SHADING_TOONY_VAL_BIT]) {
+                            toonSchema._shadingToony = material->getShadingToony();
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::SHADING_TOONY_VAL_BIT, true);
+                            wasSet = true;
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_SCROLL_VAL_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_SCROLL_VAL_BIT]) {
+                            toonSchema._uvAnimationScrollSpeed.x = material->getUVAnimationScrollXSpeed();
+                            toonSchema._uvAnimationScrollSpeed.y = material->getUVAnimationScrollYSpeed();
+                            toonSchema._uvAnimationScrollRotationSpeed = material->getUVAnimationRotationSpeed();
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_SCROLL_VAL_BIT, true);
+                            wasSet = true;
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::SHADE_MAP_BIT :
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::SHADE_MAP_BIT]) {
+                            auto itr = textureMaps.find((graphics::Material::MapChannel) NetworkMToonMaterial::SHADE_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialShade, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
+                                }
+                            } else {
+                                forceDefault = true;
+                            }
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::SHADE_MAP_BIT, true);
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::SHADING_SHIFT_MAP_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::SHADING_SHIFT_MAP_BIT]) {
+                            auto itr = textureMaps.find((graphics::Material::MapChannel) NetworkMToonMaterial::SHADING_SHIFT_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialShadingShift, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
+                                }
+                            } else {
+                                forceDefault = true;
+                            }
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::SHADING_SHIFT_MAP_BIT, true);
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::MATCAP_MAP_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::MATCAP_MAP_BIT]) {
+                            auto itr = textureMaps.find((graphics::Material::MapChannel) NetworkMToonMaterial::MATCAP_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialMatcap, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
+                                }
+                            } else {
+                                forceDefault = true;
+                            }
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::MATCAP_MAP_BIT, true);
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::RIM_MAP_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::RIM_MAP_BIT]) {
+                            auto itr = textureMaps.find((graphics::Material::MapChannel) NetworkMToonMaterial::RIM_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialRim, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
+                                }
+                            } else {
+                                forceDefault = true;
+                            }
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::RIM_MAP_BIT, true);
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_MASK_MAP_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_MASK_MAP_BIT]) {
+                            auto itr = textureMaps.find((graphics::Material::MapChannel) NetworkMToonMaterial::UV_ANIMATION_MASK_MAP);
+                            if (itr != textureMaps.end()) {
+                                if (itr->second->isDefined()) {
+                                    drawMaterialTextures->setTexture(gr::Texture::MaterialUVAnimationMask, itr->second->getTextureView());
+                                    if (itr->second->getTextureView().isReference()) {
+                                        multiMaterial.addReferenceTexture(itr->second->getTextureView().getTextureOperator());
+                                    }
+                                    wasSet = true;
+                                } else {
+                                    multiMaterial.setTexturesLoading(true);
+                                    forceDefault = true;
+                                }
+                            } else {
+                                forceDefault = true;
+                            }
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_MASK_MAP_BIT, true);
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::MATCAP_VAL_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::MATCAP_VAL_BIT]) {
+                            toonSchema._matcap = material->getMatcap(false);
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::MATCAP_VAL_BIT, true);
+                            wasSet = true;
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::PARAMETRIC_RIM_VAL_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::PARAMETRIC_RIM_VAL_BIT]) {
+                            toonSchema._parametricRim = material->getParametricRim(false);
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::PARAMETRIC_RIM_VAL_BIT, true);
+                            wasSet = true;
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::PARAMETRIC_RIM_POWER_VAL_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::PARAMETRIC_RIM_POWER_VAL_BIT]) {
+                            toonSchema._parametricRimFresnelPower = material->getParametricRimFresnelPower();
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::PARAMETRIC_RIM_POWER_VAL_BIT, true);
+                            wasSet = true;
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::PARAMETRIC_RIM_LIFT_VAL_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::PARAMETRIC_RIM_LIFT_VAL_BIT]) {
+                            toonSchema._parametricRimLift = material->getParametricRimLift();
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::PARAMETRIC_RIM_LIFT_VAL_BIT, true);
+                            wasSet = true;
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::RIM_LIGHTING_MIX_VAL_BIT:
+                        if (materialKey._flags[NetworkMToonMaterial::MToonFlagBit::RIM_LIGHTING_MIX_VAL_BIT]) {
+                            toonSchema._rimLightingMix = material->getRimLightingMix();
+                            schemaKey._flags.set(NetworkMToonMaterial::MToonFlagBit::RIM_LIGHTING_MIX_VAL_BIT, true);
+                            wasSet = true;
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::OUTLINE_WIDTH_MODE_VAL_BIT:
+                        if (!fallthrough) {
+                            multiMaterial.setOutlineWidthMode(material->getOutlineWidthMode());
+                            wasSet = true;
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::OUTLINE_WIDTH_VAL_BIT:
+                        if (!fallthrough) {
+                            multiMaterial.setOutlineWidth(material->getOutlineWidth());
+                            wasSet = true;
+                        }
+                        break;
+                    case NetworkMToonMaterial::MToonFlagBit::OUTLINE_VAL_BIT:
+                        if (!fallthrough) {
+                            multiMaterial.setOutline(material->getOutline(false));
+                            wasSet = true;
+                        }
+                        break;
+                    default:
+                        break;
+                }
             }
 
             if (wasSet) {
@@ -744,71 +1167,115 @@ void RenderPipelines::updateMultiMaterial(graphics::MultiMaterial& multiMaterial
     auto textureCache = DependencyManager::get<TextureCache>();
     // Handle defaults
     for (auto flag : flagsToSetDefault) {
-        switch (flag) {
-            case graphics::MaterialKey::EMISSIVE_VAL_BIT:
-            case graphics::MaterialKey::UNLIT_VAL_BIT:
-            case graphics::MaterialKey::ALBEDO_VAL_BIT:
-            case graphics::MaterialKey::METALLIC_VAL_BIT:
-            case graphics::MaterialKey::GLOSSY_VAL_BIT:
-            case graphics::MaterialKey::OPACITY_VAL_BIT:
-            case graphics::MaterialKey::OPACITY_CUTOFF_VAL_BIT:
-            case graphics::MaterialKey::SCATTERING_VAL_BIT:
-            case graphics::Material::TEXCOORDTRANSFORM0:
-            case graphics::Material::TEXCOORDTRANSFORM1:
-            case graphics::Material::LIGHTMAP_PARAMS:
-            case graphics::Material::MATERIAL_PARAMS:
-                // these are initialized to the correct default values in Schema()
-                break;
-            case graphics::Material::CULL_FACE_MODE:
-                multiMaterial.setCullFaceMode(graphics::Material::DEFAULT_CULL_FACE_MODE);
-                break;
-            case graphics::MaterialKey::ALBEDO_MAP_BIT:
-                if (schemaKey.isAlbedoMap()) {
-                    drawMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, textureCache->getWhiteTexture());
-                }
-                break;
-            case graphics::MaterialKey::METALLIC_MAP_BIT:
-                if (schemaKey.isMetallicMap()) {
-                    drawMaterialTextures->setTexture(gr::Texture::MaterialMetallic, textureCache->getBlackTexture());
-                }
-                break;
-            case graphics::MaterialKey::ROUGHNESS_MAP_BIT:
-                if (schemaKey.isRoughnessMap()) {
-                    drawMaterialTextures->setTexture(gr::Texture::MaterialRoughness, textureCache->getWhiteTexture());
-                }
-                break;
-            case graphics::MaterialKey::NORMAL_MAP_BIT:
-                if (schemaKey.isNormalMap()) {
-                    drawMaterialTextures->setTexture(gr::Texture::MaterialNormal, textureCache->getBlueTexture());
-                }
-                break;
-            case graphics::MaterialKey::OCCLUSION_MAP_BIT:
-                if (schemaKey.isOcclusionMap()) {
-                    drawMaterialTextures->setTexture(gr::Texture::MaterialOcclusion, textureCache->getWhiteTexture());
-                }
-                break;
-            case graphics::MaterialKey::SCATTERING_MAP_BIT:
-                if (schemaKey.isScatteringMap()) {
-                    drawMaterialTextures->setTexture(gr::Texture::MaterialScattering, textureCache->getWhiteTexture());
-                }
-                break;
-            case graphics::MaterialKey::EMISSIVE_MAP_BIT:
-                if (schemaKey.isEmissiveMap() && !schemaKey.isLightMap()) {
-                    drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getGrayTexture());
-                }
-                break;
-            case graphics::MaterialKey::LIGHT_MAP_BIT:
-                if (schemaKey.isLightMap()) {
-                    drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getBlackTexture());
-                }
-                break;
-            default:
-                break;
+        if (!multiMaterial.isMToon()) {
+            switch (flag) {
+                case graphics::Material::CULL_FACE_MODE:
+                    multiMaterial.setCullFaceMode(graphics::Material::DEFAULT_CULL_FACE_MODE);
+                    break;
+                case graphics::MaterialKey::ALBEDO_MAP_BIT:
+                    if (schemaKey.isAlbedoMap()) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, textureCache->getWhiteTexture());
+                    }
+                    break;
+                case graphics::MaterialKey::METALLIC_MAP_BIT:
+                    if (schemaKey.isMetallicMap()) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialMetallic, textureCache->getBlackTexture());
+                    }
+                    break;
+                case graphics::MaterialKey::ROUGHNESS_MAP_BIT:
+                    if (schemaKey.isRoughnessMap()) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialRoughness, textureCache->getWhiteTexture());
+                    }
+                    break;
+                case graphics::MaterialKey::NORMAL_MAP_BIT:
+                    if (schemaKey.isNormalMap()) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialNormal, textureCache->getBlueTexture());
+                    }
+                    break;
+                case graphics::MaterialKey::OCCLUSION_MAP_BIT:
+                    if (schemaKey.isOcclusionMap()) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialOcclusion, textureCache->getWhiteTexture());
+                    }
+                    break;
+                case graphics::MaterialKey::SCATTERING_MAP_BIT:
+                    if (schemaKey.isScatteringMap()) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialScattering, textureCache->getWhiteTexture());
+                    }
+                    break;
+                case graphics::MaterialKey::EMISSIVE_MAP_BIT:
+                    if (schemaKey.isEmissiveMap() && !schemaKey.isLightMap()) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getGrayTexture());
+                    }
+                    break;
+                case graphics::MaterialKey::LIGHT_MAP_BIT:
+                    if (schemaKey.isLightMap()) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getBlackTexture());
+                    }
+                    break;
+                default:
+                    // everything else is initialized to the correct default values in Schema()
+                    break;
+            }
+        } else {
+            switch (flag) {
+                case graphics::Material::CULL_FACE_MODE:
+                    multiMaterial.setCullFaceMode(graphics::Material::DEFAULT_CULL_FACE_MODE);
+                    break;
+                case graphics::MaterialKey::ALBEDO_MAP_BIT:
+                    if (schemaKey.isAlbedoMap()) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, textureCache->getWhiteTexture());
+                    }
+                    break;
+                case graphics::MaterialKey::NORMAL_MAP_BIT:
+                    if (schemaKey.isNormalMap()) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialNormal, textureCache->getBlueTexture());
+                    }
+                    break;
+                case graphics::MaterialKey::EMISSIVE_MAP_BIT:
+                    if (schemaKey.isEmissiveMap() && !schemaKey.isLightMap()) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getGrayTexture());
+                    }
+                    break;
+                case NetworkMToonMaterial::MToonFlagBit::SHADE_MAP_BIT:
+                    if (schemaKey._flags[NetworkMToonMaterial::MToonFlagBit::SHADE_MAP_BIT]) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialShade, textureCache->getWhiteTexture());
+                    }
+                    break;
+                case NetworkMToonMaterial::MToonFlagBit::SHADING_SHIFT_MAP_BIT:
+                    if (schemaKey._flags[NetworkMToonMaterial::MToonFlagBit::SHADING_SHIFT_MAP_BIT]) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialShadingShift, textureCache->getBlackTexture());
+                    }
+                    break;
+                case NetworkMToonMaterial::MToonFlagBit::MATCAP_MAP_BIT:
+                    if (schemaKey._flags[NetworkMToonMaterial::MToonFlagBit::MATCAP_MAP_BIT]) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialMatcap, textureCache->getBlackTexture());
+                    }
+                    break;
+                case NetworkMToonMaterial::MToonFlagBit::RIM_MAP_BIT:
+                    if (schemaKey._flags[NetworkMToonMaterial::MToonFlagBit::RIM_MAP_BIT]) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialRim, textureCache->getWhiteTexture());
+                    }
+                    break;
+                case NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_MASK_MAP_BIT:
+                    if (schemaKey._flags[NetworkMToonMaterial::MToonFlagBit::UV_ANIMATION_MASK_MAP_BIT]) {
+                        drawMaterialTextures->setTexture(gr::Texture::MaterialUVAnimationMask, textureCache->getWhiteTexture());
+                    }
+                    break;
+                default:
+                    // everything else is initialized to the correct default values in ToonSchema()
+                    break;
+            }
         }
     }
 
-    schema._key = (uint32_t)schemaKey._flags.to_ulong();
-    schemaBuffer.edit<graphics::MultiMaterial::Schema>() = schema;
+    auto& schemaBuffer = multiMaterial.getSchemaBuffer();
+    if (multiMaterial.isMToon()) {
+        toonSchema._key = (uint32_t)schemaKey._flags.to_ulong();
+        schemaBuffer.edit<graphics::MultiMaterial::MToonSchema>() = toonSchema;
+    } else {
+        schema._key = (uint32_t)schemaKey._flags.to_ulong();
+        schemaBuffer.edit<graphics::MultiMaterial::Schema>() = schema;
+    }
     multiMaterial.setNeedsUpdate(false);
     multiMaterial.setInitialized();
 }
@@ -818,10 +1285,16 @@ bool RenderPipelines::bindMaterials(graphics::MultiMaterial& multiMaterial, gpu:
         updateMultiMaterial(multiMaterial);
     }
 
+    if (multiMaterial.isMToon()) {
+        multiMaterial.setMToonTime();
+    }
+
     auto textureCache = DependencyManager::get<TextureCache>();
 
     static gpu::TextureTablePointer defaultMaterialTextures = std::make_shared<gpu::TextureTable>();
     static gpu::BufferView defaultMaterialSchema;
+    static gpu::TextureTablePointer defaultMToonMaterialTextures = std::make_shared<gpu::TextureTable>();
+    static gpu::BufferView defaultMToonMaterialSchema;
 
     static std::once_flag once;
     std::call_once(once, [textureCache] {
@@ -835,6 +1308,18 @@ bool RenderPipelines::bindMaterials(graphics::MultiMaterial& multiMaterial, gpu:
         defaultMaterialTextures->setTexture(gr::Texture::MaterialOcclusion, textureCache->getWhiteTexture());
         defaultMaterialTextures->setTexture(gr::Texture::MaterialScattering, textureCache->getWhiteTexture());
         // MaterialEmissiveLightmap has to be set later
+
+        graphics::MultiMaterial::MToonSchema toonSchema;
+        defaultMToonMaterialSchema = gpu::BufferView(std::make_shared<gpu::Buffer>(sizeof(toonSchema), (const gpu::Byte*) &toonSchema, sizeof(toonSchema)));
+
+        defaultMToonMaterialTextures->setTexture(gr::Texture::MaterialAlbedo, textureCache->getWhiteTexture());
+        defaultMToonMaterialTextures->setTexture(gr::Texture::MaterialNormal, textureCache->getBlueTexture());
+        defaultMToonMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getGrayTexture());
+        defaultMToonMaterialTextures->setTexture(gr::Texture::MaterialShade, textureCache->getWhiteTexture());
+        defaultMToonMaterialTextures->setTexture(gr::Texture::MaterialShadingShift, textureCache->getBlackTexture());
+        defaultMToonMaterialTextures->setTexture(gr::Texture::MaterialMatcap, textureCache->getBlackTexture());
+        defaultMToonMaterialTextures->setTexture(gr::Texture::MaterialRim, textureCache->getWhiteTexture());
+        defaultMToonMaterialTextures->setTexture(gr::Texture::MaterialUVAnimationMask, textureCache->getWhiteTexture());
     });
 
     // For shadows, we only need opacity mask information
@@ -845,20 +1330,24 @@ bool RenderPipelines::bindMaterials(graphics::MultiMaterial& multiMaterial, gpu:
         if (enableTextures) {
             batch.setResourceTextureTable(multiMaterial.getTextureTable());
         } else {
-            if (renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) {
-                if (key.isLightMap()) {
-                    defaultMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getBlackTexture());
-                } else if (key.isEmissiveMap()) {
-                    defaultMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getGrayTexture());
+            if (!multiMaterial.isMToon()) {
+                if (renderMode != render::Args::RenderMode::SHADOW_RENDER_MODE) {
+                    if (key.isLightMap()) {
+                        defaultMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getBlackTexture());
+                    } else if (key.isEmissiveMap()) {
+                        defaultMaterialTextures->setTexture(gr::Texture::MaterialEmissiveLightmap, textureCache->getGrayTexture());
+                    }
                 }
-            }
 
-            batch.setResourceTextureTable(defaultMaterialTextures);
+                batch.setResourceTextureTable(defaultMaterialTextures);
+            } else {
+                batch.setResourceTextureTable(defaultMToonMaterialTextures);
+            }
         }
         return true;
     } else {
-        batch.setResourceTextureTable(defaultMaterialTextures);
-        batch.setUniformBuffer(gr::Buffer::Material, defaultMaterialSchema);
+        batch.setResourceTextureTable(!multiMaterial.isMToon() ? defaultMaterialTextures : defaultMToonMaterialTextures);
+        batch.setUniformBuffer(gr::Buffer::Material, !multiMaterial.isMToon() ? defaultMaterialSchema : defaultMToonMaterialSchema);
         return false;
     }
 }
diff --git a/libraries/render-utils/src/RenderShadowTask.cpp b/libraries/render-utils/src/RenderShadowTask.cpp
index 9fabad5fd0..d6c50bc6fb 100644
--- a/libraries/render-utils/src/RenderShadowTask.cpp
+++ b/libraries/render-utils/src/RenderShadowTask.cpp
@@ -37,6 +37,7 @@
 using namespace render;
 
 extern void initZPassPipelines(ShapePlumber& plumber, gpu::StatePointer state, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter);
+extern void sortAndRenderZPassShapes(const ShapePlumberPointer& shapePlumber, const render::RenderContextPointer& renderContext, const render::ShapeBounds& inShapes, render::ItemBounds &itemBounds);
 
 void RenderShadowTask::configure(const Config& configuration) {
     //DependencyManager::get<DeferredLightingEffect>()->setShadowMapEnabled(configuration.isEnabled());
@@ -49,13 +50,10 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende
     static ShapePlumberPointer shapePlumber = std::make_shared<ShapePlumber>();
     static std::once_flag once;
     std::call_once(once, [] {
-        auto state = std::make_shared<gpu::State>();
-        state->setCullMode(gpu::State::CULL_BACK);
-        state->setDepthTest(true, true, gpu::LESS_EQUAL);
-
         auto fadeEffect = DependencyManager::get<FadeEffect>();
-        initZPassPipelines(*shapePlumber, state, fadeEffect->getBatchSetter(), fadeEffect->getItemUniformSetter());
+        initZPassPipelines(*shapePlumber, std::make_shared<gpu::State>(), fadeEffect->getBatchSetter(), fadeEffect->getItemUniformSetter());
     });
+
     const auto setupOutput = task.addJob<RenderShadowSetup>("ShadowSetup", input);
     const auto queryResolution = setupOutput.getN<RenderShadowSetup::Output>(1);
     const auto shadowFrame = setupOutput.getN<RenderShadowSetup::Output>(3);
@@ -254,58 +252,8 @@ void RenderShadowMap::run(const render::RenderContextPointer& renderContext, con
             batch.setProjectionTransform(projMat);
             batch.setViewTransform(viewMat, false);
 
-            const std::vector<ShapeKey::Builder> keys = {
-                ShapeKey::Builder(), ShapeKey::Builder().withFade(),
-                ShapeKey::Builder().withDeformed(), ShapeKey::Builder().withDeformed().withFade(),
-                ShapeKey::Builder().withDeformed().withDualQuatSkinned(), ShapeKey::Builder().withDeformed().withDualQuatSkinned().withFade(),
-                ShapeKey::Builder().withOwnPipeline(), ShapeKey::Builder().withOwnPipeline().withFade(),
-                ShapeKey::Builder().withDeformed().withOwnPipeline(), ShapeKey::Builder().withDeformed().withOwnPipeline().withFade(),
-                ShapeKey::Builder().withDeformed().withDualQuatSkinned().withOwnPipeline(), ShapeKey::Builder().withDeformed().withDualQuatSkinned().withOwnPipeline().withFade(),
-            };
-            std::vector<std::vector<ShapeKey>> sortedShapeKeys(keys.size());
-
-            const int OWN_PIPELINE_INDEX = 6;
-            for (const auto& items : inShapes) {
-                int index = items.first.hasOwnPipeline() ? OWN_PIPELINE_INDEX : 0;
-                if (items.first.isDeformed()) {
-                    index += 2;
-                    if (items.first.isDualQuatSkinned()) {
-                        index += 2;
-                    }
-                }
-
-                if (items.first.isFaded()) {
-                    index += 1;
-                }
-
-                sortedShapeKeys[index].push_back(items.first);
-            }
-
-            // Render non-withOwnPipeline things
-            for (size_t i = 0; i < OWN_PIPELINE_INDEX; i++) {
-                auto& shapeKeys = sortedShapeKeys[i];
-                if (shapeKeys.size() > 0) {
-                    const auto& shapePipeline = _shapePlumber->pickPipeline(args, keys[i]);
-                    args->_shapePipeline = shapePipeline;
-                    for (const auto& key : shapeKeys) {
-                        renderShapes(renderContext, _shapePlumber, inShapes.at(key));
-                    }
-                }
-            }
-
-            // Render withOwnPipeline things
-            for (size_t i = OWN_PIPELINE_INDEX; i < keys.size(); i++) {
-                auto& shapeKeys = sortedShapeKeys[i];
-                if (shapeKeys.size() > 0) {
-                    args->_shapePipeline = nullptr;
-                    for (const auto& key : shapeKeys) {
-                        args->_itemShapeKey = key._flags.to_ulong();
-                        renderShapes(renderContext, _shapePlumber, inShapes.at(key));
-                    }
-                }
-            }
-
-            args->_shapePipeline = nullptr;
+            render::ItemBounds itemBounds;
+            sortAndRenderZPassShapes(_shapePlumber, renderContext, inShapes, itemBounds);
         }
 
         args->_batch = nullptr;
diff --git a/libraries/render-utils/src/model.slf b/libraries/render-utils/src/model.slf
index 2f80fbde99..48e483edf7 100644
--- a/libraries/render-utils/src/model.slf
+++ b/libraries/render-utils/src/model.slf
@@ -5,6 +5,7 @@
 //
 //  Created by Andrzej Kapolka on 5/6/14.
 //  Copyright 2014 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -15,8 +16,51 @@
 <@include render-utils/ShaderConstants.h@>
 <@include CullFace.slh@>
 
+<@if HIFI_USE_MTOON@>
+    <@if HIFI_USE_SHADOW@>
+        <$declareMToonMaterialTextures(ALBEDO)$>
+        <@if HIFI_USE_MIRROR@>
+            LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_MIRROR) uniform sampler2D mirrorMap;
+        <@endif@>
+    <@else@>
+        <$declareMToonMaterialTextures(ALBEDO, HIFI_USE_NORMALMAP, SHADE, EMISSIVE, SHADING_SHIFT, MATCAP, RIM, UV_ANIMATION_MASK)$>
+    <@endif@>
+<@else@>
+    <@if HIFI_USE_SHADOW or HIFI_USE_UNLIT@>
+        <$declareMaterialTextures(ALBEDO)$>
+        <@if HIFI_USE_MIRROR@>
+            LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_MIRROR) uniform sampler2D mirrorMap;
+        <@endif@>
+    <@else@>
+        <@if not HIFI_USE_LIGHTMAP@>
+            <@if HIFI_USE_TRANSLUCENT@>
+                <$declareMaterialTextures(ALBEDO, ROUGHNESS, HIFI_USE_NORMALMAP, METALLIC, EMISSIVE, OCCLUSION)$>
+            <@else@>
+                <$declareMaterialTextures(ALBEDO, ROUGHNESS, HIFI_USE_NORMALMAP, METALLIC, EMISSIVE, OCCLUSION, SCATTERING)$>
+            <@endif@>
+        <@else@>
+            <$declareMaterialTextures(ALBEDO, ROUGHNESS, HIFI_USE_NORMALMAP, METALLIC)$>
+            <$declareMaterialLightmap()$>
+        <@endif@>
+    <@endif@>
+<@endif@>
+
 <@if not HIFI_USE_SHADOW@>
-    <@if HIFI_USE_FORWARD or HIFI_USE_TRANSLUCENT@>
+    <@if HIFI_USE_MTOON@>
+
+        <@include DefaultMaterials.slh@>
+        <@include GlobalLight.slh@>
+        <$declareEvalGlobalLightingAlphaBlendedMToon()$>
+
+        <@include gpu/Transform.slh@>
+        <$declareStandardCameraTransform()$>
+
+        <@if HIFI_USE_FORWARD or HIFI_USE_TRANSLUCENT@>
+            layout(location=0) out vec4 _fragColor0;
+        <@else@>
+            <@include DeferredBufferWrite.slh@>
+        <@endif@>
+    <@elif HIFI_USE_FORWARD or HIFI_USE_TRANSLUCENT@>
         <@include DefaultMaterials.slh@>
         <@include GlobalLight.slh@>
         <@if HIFI_USE_LIGHTMAP@>
@@ -31,6 +75,7 @@
         <@endif@>
         <@include gpu/Transform.slh@>
         <$declareStandardCameraTransform()$>
+
         layout(location=0) out vec4 _fragColor0;
     <@else@>
         <@include DeferredBufferWrite.slh@>
@@ -51,32 +96,6 @@
     <@include LightingModel.slh@>
 <@endif@>
 
-<@if HIFI_USE_SHADOW or HIFI_USE_UNLIT@>
-    <$declareMaterialTextures(ALBEDO)$>
-    <@if HIFI_USE_MIRROR@>
-        LAYOUT(binding=GRAPHICS_TEXTURE_MATERIAL_MIRROR) uniform sampler2D mirrorMap;
-    <@endif@>
-<@else@>
-    <@if not HIFI_USE_LIGHTMAP@>
-        <@if HIFI_USE_NORMALMAP and HIFI_USE_TRANSLUCENT@>
-            <$declareMaterialTextures(ALBEDO, ROUGHNESS, NORMAL , METALLIC, EMISSIVE, OCCLUSION)$>
-        <@elif HIFI_USE_NORMALMAP@>
-            <$declareMaterialTextures(ALBEDO, ROUGHNESS, NORMAL , METALLIC, EMISSIVE, OCCLUSION, SCATTERING)$>
-        <@elif HIFI_USE_TRANSLUCENT@>
-            <$declareMaterialTextures(ALBEDO, ROUGHNESS, _SCRIBE_NULL , METALLIC, EMISSIVE, OCCLUSION)$>
-        <@else@>
-            <$declareMaterialTextures(ALBEDO, ROUGHNESS, _SCRIBE_NULL , METALLIC, EMISSIVE, OCCLUSION, SCATTERING)$>
-        <@endif@>
-    <@else@>
-        <@if HIFI_USE_NORMALMAP@>
-            <$declareMaterialTextures(ALBEDO, ROUGHNESS, NORMAL, METALLIC)$>
-        <@else@>
-            <$declareMaterialTextures(ALBEDO, ROUGHNESS, _SCRIBE_NULL, METALLIC)$>
-        <@endif@>
-        <$declareMaterialLightmap()$>
-    <@endif@>
-<@endif@>
-
 <@if HIFI_USE_FADE@>
     <@include Fade.slh@>
     <$declareFadeFragment()$>
@@ -89,7 +108,9 @@ layout(location=RENDER_UTILS_ATTR_TEXCOORD01) in vec4 _texCoord01;
 <@if not HIFI_USE_SHADOW@>
     layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES;
     layout(location=RENDER_UTILS_ATTR_NORMAL_WS) in vec3 _normalWS;
-    layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color;
+    <@if not HIFI_USE_MTOON@>
+        layout(location=RENDER_UTILS_ATTR_COLOR) in vec4 _color;
+    <@endif@>
     <@if HIFI_USE_NORMALMAP@>
         layout(location=RENDER_UTILS_ATTR_TANGENT_WS) in vec3 _tangentWS;
     <@endif@>
@@ -112,15 +133,21 @@ void main(void) {
     Material mat = getMaterial();
     BITFIELD matKey = getMaterialKey(mat);
 <@if HIFI_USE_SHADOW or HIFI_USE_UNLIT@>
+    <@if not HIFI_USE_MTOON@>
         <$fetchMaterialTexturesCoord0(matKey, _texCoord0, albedoTex)$>
+    <@else@>
+        <$fetchMToonMaterialTexturesCoord0(matKey, _texCoord0, albedoTex)$>
+    <@endif@>
 
-    <@if HIFI_USE_TRANSLUCENT@>
         float cutoff = getMaterialOpacityCutoff(mat);
-        float opacity = getMaterialOpacity(mat) * _color.a;
+    <@if HIFI_USE_TRANSLUCENT@>
+        float opacity = getMaterialOpacity(mat);
+        <@if not HIFI_USE_MTOON@>
+            opacity *= _color.a;
+        <@endif@>
         <$evalMaterialOpacity(albedoTex.a, cutoff, opacity, matKey, opacity)$>;
         <$discardInvisible(opacity)$>;
     <@else@>
-        float cutoff = getMaterialOpacityCutoff(mat);
         float opacity = 1.0;
         <$evalMaterialOpacityMask(albedoTex.a, cutoff, opacity, matKey, opacity)$>;
         <$discardTransparent(opacity)$>;
@@ -129,7 +156,9 @@ void main(void) {
     <@if not HIFI_USE_SHADOW@>
             vec3 albedo = getMaterialAlbedo(mat);
             <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>;
-            albedo *= _color.rgb;
+            <@if not HIFI_USE_MTOON@>
+                albedo *= _color.rgb;
+            <@endif@>
         <@if HIFI_USE_FADE@>
                 albedo += fadeEmissive;
         <@endif@>
@@ -152,6 +181,73 @@ void main(void) {
                 opacity,
                 albedo * isUnlitEnabled());
     <@endif@>
+<@elif HIFI_USE_MTOON@>
+        vec3 uvScrollSpeed = getMaterialUVScrollSpeed(mat);
+        float time = getMaterialTime(mat);
+    <@if HIFI_USE_NORMALMAP@>
+        <$fetchMToonMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, normalTex, shadeTex, emissiveTex, shadingShiftTex, rimTex, uvScrollSpeed, time)$>
+    <@else@>
+        <$fetchMToonMaterialTexturesCoord0(matKey, _texCoord0, albedoTex, _SCRIBE_NULL, shadeTex, emissiveTex, shadingShiftTex, rimTex, uvScrollSpeed, time)$>
+    <@endif@>
+
+        float cutoff = getMaterialOpacityCutoff(mat);
+        <@if HIFI_USE_TRANSLUCENT@>
+            float opacity = getMaterialOpacity(mat);
+            <$evalMaterialOpacity(albedoTex.a, cutoff, opacity, matKey, opacity)$>;
+            <$discardInvisible(opacity)$>;
+        <@else@>
+            float opacity = 1.0;
+            <$evalMaterialOpacityMask(albedoTex.a, cutoff, opacity, matKey, opacity)$>;
+            <$discardTransparent(opacity)$>;
+        <@endif@>
+
+        vec3 albedo = getMaterialAlbedo(mat);
+        <$evalMaterialAlbedo(albedoTex, albedo, matKey, albedo)$>;
+
+        vec3 emissive = getMaterialEmissive(mat);
+        <$evalMaterialEmissive(emissiveTex, emissive, matKey, emissive)$>;
+
+    <@if HIFI_USE_NORMALMAP@>
+            vec3 fragNormalWS;
+            <$evalMaterialNormalLOD(_positionES, normalTex, _normalWS, _tangentWS, fragNormalWS)$>
+    <@else@>
+            vec3 fragNormalWS = _normalWS;
+    <@endif@>
+        fragNormalWS = evalFrontOrBackFaceNormal(normalize(fragNormalWS));
+
+        vec3 shade = getMaterialShade(mat);
+        <$evalMaterialShade(shadeTex, shade, matKey, shade)$>;
+
+        float shadingShift = getMaterialShadingShift(mat);
+        <$evalMaterialShadingShift(shadingShiftTex, shadingShift, matKey, shadingShift)$>;
+
+        TransformCamera cam = getTransformCamera();
+        float metallic = DEFAULT_METALLIC;
+        vec3 fresnel = getFresnelF0(metallic, albedo);
+
+        vec4 color = vec4(evalGlobalLightingAlphaBlendedMToon(
+            cam._viewInverse, 1.0, _positionES.xyz, fragNormalWS,
+            albedo, fresnel, metallic, emissive, DEFAULT_ROUGHNESS, opacity,
+            shade, shadingShift, getMaterialShadingToony(mat), getMaterialMatcap(mat), getMaterialParametricRim(mat),
+            getMaterialParametricRimFresnelPower(mat), getMaterialParametricRimLift(mat), rimTex, getMaterialRimLightingMix(mat), matKey), opacity);
+
+        <@if HIFI_USE_FORWARD or HIFI_USE_TRANSLUCENT@>
+            _fragColor0 = isUnlitEnabled() * vec4(color.rgb
+            <@if HIFI_USE_FADE@>
+                    + fadeEmissive
+            <@endif@>
+                , color.a);
+        <@else@>
+            packDeferredFragmentUnlit(
+                fragNormalWS,
+                1.0,
+                color.rgb
+            <@if HIFI_USE_FADE@>
+                    + fadeEmissive
+            <@endif@>
+                );
+        <@endif@>
+
 <@else@>
     <@if not HIFI_USE_LIGHTMAP@>
         <@if HIFI_USE_NORMALMAP and HIFI_USE_TRANSLUCENT@>
@@ -172,14 +268,13 @@ void main(void) {
         <@endif@>
             <$fetchMaterialTexturesCoord1(matKey, _texCoord1, _SCRIBE_NULL, lightmap)$>
     <@endif@>
-
-    <@if HIFI_USE_TRANSLUCENT@>
+    
         float cutoff = getMaterialOpacityCutoff(mat);
+    <@if HIFI_USE_TRANSLUCENT@>
         float opacity = getMaterialOpacity(mat) * _color.a;
         <$evalMaterialOpacity(albedoTex.a, cutoff, opacity, matKey, opacity)$>;
         <$discardInvisible(opacity)$>;
     <@else@>
-        float cutoff = getMaterialOpacityCutoff(mat);
         float opacity = 1.0;
         <$evalMaterialOpacityMask(albedoTex.a, cutoff, opacity, matKey, opacity)$>;
         <$discardTransparent(opacity)$>;
diff --git a/libraries/render-utils/src/model.slv b/libraries/render-utils/src/model.slv
index 319711eac2..848acfc331 100644
--- a/libraries/render-utils/src/model.slv
+++ b/libraries/render-utils/src/model.slv
@@ -5,6 +5,7 @@
 //
 //  Created by Hifi Engine Team
 //  Copyright 2019 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -47,7 +48,9 @@ layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01;
 <@if not HIFI_USE_SHADOW@>
     layout(location=RENDER_UTILS_ATTR_POSITION_ES) out vec4 _positionES;
     layout(location=RENDER_UTILS_ATTR_NORMAL_WS) out vec3 _normalWS;
-    layout(location=RENDER_UTILS_ATTR_COLOR) out vec4 _color;
+    <@if not HIFI_USE_MTOON@>
+        layout(location=RENDER_UTILS_ATTR_COLOR) out vec4 _color;
+    <@endif@>
     <@if HIFI_USE_NORMALMAP@>
         layout(location=RENDER_UTILS_ATTR_TANGENT_WS) out vec3 _tangentWS;
     <@endif@>
@@ -96,7 +99,9 @@ void main(void) {
             _texCoord01 = vec4(0.0);
         }
 <@else@>
+<@if not HIFI_USE_MTOON@>
         _color = color_sRGBAToLinear(inColor);
+<@endif@>
 
         TexMapArray texMapArray = getTexMapArray();
         <$evalTexMapArrayTexcoord0(texMapArray, inTexCoord0, _positionWS, _texCoord01.xy)$>
diff --git a/libraries/render-utils/src/render-utils/model.slp b/libraries/render-utils/src/render-utils/model.slp
index a3c28631e9..f2b1a0d588 100644
--- a/libraries/render-utils/src/render-utils/model.slp
+++ b/libraries/render-utils/src/render-utils/model.slp
@@ -1 +1 @@
-DEFINES (normalmap translucent:f unlit:f/lightmap:f)/(shadow mirror:f) fade:f/forward:f deformed:v/deformeddq:v
\ No newline at end of file
+DEFINES (normalmap translucent:f unlit:f/lightmap:f)/(shadow mirror:f) mtoon fade:f/forward:f deformed:v/deformeddq:v
diff --git a/libraries/render-utils/src/sdf_text3D.slf b/libraries/render-utils/src/sdf_text3D.slf
index bf9bb0babd..f606c1be81 100644
--- a/libraries/render-utils/src/sdf_text3D.slf
+++ b/libraries/render-utils/src/sdf_text3D.slf
@@ -32,6 +32,7 @@
 <@include sdf_text3D.slh@>
 <$declareEvalSDFSuperSampled()$>
 
+layout(location=RENDER_UTILS_ATTR_POSITION_MS) in vec2 _positionMS;
 <@if HIFI_USE_TRANSLUCENT or HIFI_USE_FORWARD@>
     layout(location=RENDER_UTILS_ATTR_POSITION_ES) in vec4 _positionES;
 <@endif@>
@@ -48,7 +49,7 @@ layout(location=RENDER_UTILS_ATTR_FADE1) flat in vec4 _glyphBounds; // we're reu
 <@endif@>
 
 void main() {
-    vec4 color = evalSDFSuperSampled(_texCoord0, _glyphBounds);
+    vec4 color = evalSDFSuperSampled(_texCoord0, _positionMS, _glyphBounds);
 
 <@if HIFI_USE_TRANSLUCENT or HIFI_USE_FORWARD@>
     color.a *= params.color.a;
diff --git a/libraries/render-utils/src/sdf_text3D.slh b/libraries/render-utils/src/sdf_text3D.slh
index 76ace99182..c927070a4d 100644
--- a/libraries/render-utils/src/sdf_text3D.slh
+++ b/libraries/render-utils/src/sdf_text3D.slh
@@ -17,13 +17,16 @@
 LAYOUT(binding=0) uniform sampler2D fontTexture;
 
 struct TextParams {
+    vec4 bounds;
     vec4 color;
 
-    vec3 effectColor;
-    float effectThickness;
+    vec2 unitRange;
 
     int effect;
-    vec3 spare;
+    float effectThickness;
+
+    vec3 effectColor;
+    float spare;
 };
 
 LAYOUT(binding=0) uniform textParamsBuffer {
@@ -37,51 +40,75 @@ LAYOUT(binding=0) uniform textParamsBuffer {
 const float interiorCutoff = 0.5;
 const float taaBias = pow(2.0, TAA_TEXTURE_LOD_BIAS);
 
-vec4 evalSDF(vec2 texCoord, vec4 glyphBounds) {
+// MSDF logic from: https://github.com/Chlumsky/msdfgen?tab=readme-ov-file#using-a-multi-channel-distance-field
+float median(float r, float g, float b) {
+    return max(min(r, g), min(max(r, g), b));
+}
+
+float screenPxRange(vec2 texCoord) {
+    vec2 screenTexSize = vec2(1.0) / fwidth(texCoord);
+    return max(0.5 * dot(params.unitRange, screenTexSize), 1.0);
+}
+
+vec2 evalSDF(vec2 texCoord) {
+    vec4 msdf = textureLod(fontTexture, texCoord, TAA_TEXTURE_LOD_BIAS);
+    float sdf = median(msdf.r, msdf.g, msdf.b);
+    float screenPxDistance = screenPxRange(texCoord) * (sdf - 0.5);
+    float opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0);
+    return vec2(opacity, msdf.a);
+}
+
+vec4 evalSDFColor(vec2 texCoord, vec4 glyphBounds) {
     vec3 color = params.color.rgb;
-    float sdf = textureLod(fontTexture, texCoord, TAA_TEXTURE_LOD_BIAS).g;
+    vec2 sdf = evalSDF(texCoord);
 
     // Outline
     if (params.effect == 1 || params.effect == 2) {
-        float outline = float(sdf < interiorCutoff);
+        float outline = float(sdf.x < interiorCutoff);
         color = mix(color, params.effectColor, outline);
 
         // with or without fill
-        sdf = mix(sdf, 0.0, float(params.effect == 1) * (1.0 - outline));
+        sdf.x = mix(sdf.y, 0.0, float(params.effect == 1) * (1.0 - outline));
 
         const float EPSILON = 0.00001;
-        sdf += mix(0.0, params.effectThickness - EPSILON, outline);
+        sdf.x += mix(0.0, params.effectThickness - EPSILON, outline);
     } else if (params.effect == 3) { // Shadow
         // don't sample from outside of our glyph bounds
-        sdf *= mix(1.0, 0.0, float(clamp(texCoord, glyphBounds.xy, glyphBounds.xy + glyphBounds.zw) != texCoord));
+        sdf.x *= mix(1.0, 0.0, float(clamp(texCoord, glyphBounds.xy, glyphBounds.xy + glyphBounds.zw) != texCoord));
 
-        if (sdf < interiorCutoff) {
+        if (sdf.x < interiorCutoff) {
             color = params.effectColor;
             const float DOUBLE_MAX_OFFSET_PIXELS = 20.0; // must match value in Font.cpp
             // FIXME: TAA_TEXTURE_LOD_BIAS doesn't have any effect because we're only generating one mip, so here we need to use 0, but it should
             // really match the LOD that we use in the textureLod call below
             vec2 textureOffset = vec2(params.effectThickness * DOUBLE_MAX_OFFSET_PIXELS) / vec2(textureSize(fontTexture, 0/*int(TAA_TEXTURE_LOD_BIAS)*/));
             vec2 shadowTexCoords = texCoord - textureOffset;
-            sdf = textureLod(fontTexture, shadowTexCoords, TAA_TEXTURE_LOD_BIAS).g;
+            sdf.x = evalSDF(shadowTexCoords).x;
 
             // don't sample from outside of our glyph bounds
-            sdf *= mix(1.0, 0.0, float(clamp(shadowTexCoords, glyphBounds.xy, glyphBounds.xy + glyphBounds.zw) != shadowTexCoords));
+            sdf.x *= mix(1.0, 0.0, float(clamp(shadowTexCoords, glyphBounds.xy, glyphBounds.xy + glyphBounds.zw) != shadowTexCoords));
         }
     }
 
-    return vec4(color, sdf);
+    return vec4(color, sdf.x);
 }
 
-vec4 evalSDFSuperSampled(vec2 texCoord, vec4 glyphBounds) {
+vec4 evalSDFSuperSampled(vec2 texCoord, vec2 positionMS, vec4 glyphBounds) {
+    // Clip to edges. Note: We don't need to check the top edge.
+    if (positionMS.x < params.bounds.x || positionMS.x > (params.bounds.x + params.bounds.z) ||
+        positionMS.y < params.bounds.y - params.bounds.w) {
+        return vec4(0.0);
+    }
+
     vec2 dxTexCoord = dFdx(texCoord) * 0.5 * taaBias;
     vec2 dyTexCoord = dFdy(texCoord) * 0.5 * taaBias;
 
     // Perform 4x supersampling for anisotropic filtering
     vec4 color;
-    color = evalSDF(texCoord, glyphBounds);
-    color += evalSDF(texCoord + dxTexCoord, glyphBounds);
-    color += evalSDF(texCoord + dyTexCoord, glyphBounds);
-    color += evalSDF(texCoord + dxTexCoord + dyTexCoord, glyphBounds);
+    color = evalSDFColor(texCoord, glyphBounds);
+    color += evalSDFColor(texCoord + dxTexCoord, glyphBounds);
+    color += evalSDFColor(texCoord + dyTexCoord, glyphBounds);
+    color += evalSDFColor(texCoord + dxTexCoord + dyTexCoord, glyphBounds);
     color *= 0.25;
 
     return vec4(color.rgb, step(interiorCutoff, color.a));
diff --git a/libraries/render-utils/src/sdf_text3D.slv b/libraries/render-utils/src/sdf_text3D.slv
index 9ac3b871f9..db13e170e9 100644
--- a/libraries/render-utils/src/sdf_text3D.slv
+++ b/libraries/render-utils/src/sdf_text3D.slv
@@ -19,6 +19,7 @@
 
 <@include sdf_text3D.slh@>
 
+layout(location=RENDER_UTILS_ATTR_POSITION_MS) out vec2 _positionMS;
 <@if HIFI_USE_TRANSLUCENT or HIFI_USE_FORWARD@>
     layout(location=RENDER_UTILS_ATTR_POSITION_ES) out vec4 _positionES;
 <@endif@>
@@ -27,6 +28,7 @@ layout(location=RENDER_UTILS_ATTR_TEXCOORD01) out vec4 _texCoord01;
 layout(location=RENDER_UTILS_ATTR_FADE1) flat out vec4 _glyphBounds; // we're reusing the fade texcoord locations here
 
 void main() {
+    _positionMS = inPosition.xy;
     _texCoord01 = vec4(inTexCoord0.st, 0.0, 0.0);
     _glyphBounds = inTexCoord1;
 
diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp
index dd7074a071..3019b8f1c3 100644
--- a/libraries/render-utils/src/text/Font.cpp
+++ b/libraries/render-utils/src/text/Font.cpp
@@ -13,6 +13,10 @@
 #include <QFile>
 #include <QImage>
 #include <QNetworkReply>
+#include <QThreadStorage>
+
+#include "artery-font/artery-font.h"
+#include "artery-font/std-artery-font.h"
 
 #include <ColorUtils.h>
 
@@ -77,12 +81,150 @@ struct QuadBuilder {
     }
 };
 
-Font::Pointer Font::load(QIODevice& fontFile) {
-    Pointer font = std::make_shared<Font>();
+Font::Pointer Font::load(const QString& family, QIODevice& fontFile) {
+    Pointer font = std::make_shared<Font>(family);
     font->read(fontFile);
     return font;
 }
 
+void Font::handleFontNetworkReply() {
+    auto requestReply = qobject_cast<QNetworkReply*>(sender());
+    Q_ASSERT(requestReply != nullptr);
+
+    if (requestReply->error() == QNetworkReply::NoError) {
+        read(*requestReply);
+    } else {
+        qDebug() << "Error downloading " << requestReply->url() << " - " << requestReply->errorString();
+    }
+}
+
+QThreadStorage<size_t> _readOffset;
+QThreadStorage<size_t> _readMax;
+int readHelper(void* dst, int length, void* data) {
+    if (_readOffset.localData() + length > _readMax.localData()) {
+        return -1;
+    }
+    memcpy(dst, (char *)data + _readOffset.localData(), length);
+    _readOffset.setLocalData(_readOffset.localData() + length);
+    return length;
+};
+
+void Font::read(QIODevice& in) {
+    _loaded = false;
+
+    QByteArray data = in.readAll();
+    _readOffset.setLocalData(0);
+    _readMax.setLocalData(data.length());
+    artery_font::StdArteryFont<float> arteryFont;
+    bool success = artery_font::decode<&readHelper, float, artery_font::StdList, artery_font::StdByteArray, artery_font::StdString>(arteryFont, (void *)data.data());
+
+    if (!success) {
+        qDebug() << "Font" << _family << "failed to decode.";
+        return;
+    }
+
+    if (arteryFont.variants.length() == 0) {
+        qDebug() << "Font" << _family << "has 0 variants.";
+        return;
+    }
+
+    _distanceRange = glm::vec2(arteryFont.variants[0].metrics.distanceRange);
+    _fontSize = arteryFont.variants[0].metrics.ascender + fabs(arteryFont.variants[0].metrics.descender);
+    _leading = arteryFont.variants[0].metrics.lineHeight;
+    _spaceWidth = 0.5f * arteryFont.variants[0].metrics.emSize; // We use half the emSize as a first guess for _spaceWidth
+
+    if (arteryFont.variants[0].glyphs.length() == 0) {
+        qDebug() << "Font" << _family << "has 0 glyphs.";
+        return;
+    }
+
+    QVector<Glyph> glyphs;
+    glyphs.reserve(arteryFont.variants[0].glyphs.length());
+    for (int i = 0; i < arteryFont.variants[0].glyphs.length(); i++) {
+        auto& g = arteryFont.variants[0].glyphs[i];
+
+        Glyph glyph;
+        glyph.c = g.codepoint;
+        glyph.texOffset = glm::vec2(g.imageBounds.l, g.imageBounds.b);
+        glyph.texSize = glm::vec2(g.imageBounds.r, g.imageBounds.t) - glyph.texOffset;
+        glyph.offset = glm::vec2(g.planeBounds.l, g.planeBounds.b);
+        glyph.size = glm::vec2(g.planeBounds.r, g.planeBounds.t) - glyph.offset;
+        glyph.d = g.advance.h;
+        glyphs.push_back(glyph);
+
+        // If we find the space character, we save its size in _spaceWidth for later
+        if (glyph.c == ' ') {
+            _spaceWidth = glyph.d;
+        }
+    }
+
+    if (arteryFont.images.length() == 0) {
+        qDebug() << "Font" << _family << "has 0 images.";
+        return;
+    }
+
+    if (arteryFont.images[0].imageType != artery_font::ImageType::IMAGE_MTSDF) {
+        qDebug() << "Font" << _family << "has the wrong image type.  Expected MTSDF (7), got" << arteryFont.images[0].imageType;
+        return;
+    }
+
+    if (arteryFont.images[0].encoding != artery_font::ImageEncoding::IMAGE_PNG) {
+        qDebug() << "Font" << _family << "has the wrong encoding.  Expected PNG (8), got" << arteryFont.images[0].encoding;
+        return;
+    }
+
+    if (arteryFont.images[0].pixelFormat != artery_font::PixelFormat::PIXEL_UNSIGNED8) {
+        qDebug() << "Font" << _family << "has the wrong pixel format.  Expected unsigned char (8), got" << arteryFont.images[0].pixelFormat;
+        return;
+    }
+
+    if (arteryFont.images[0].width == 0 || arteryFont.images[0].height == 0) {
+        qDebug() << "Font" << _family << "has image with width or height of 0.  Width:" << arteryFont.images[0].width << ", height:"<< arteryFont.images[0].height;
+        return;
+    }
+
+    // read image data
+    QImage image;
+    if (!image.loadFromData((const unsigned char*)arteryFont.images[0].data, arteryFont.images[0].data.length(), "PNG")) {
+        qDebug() << "Failed to read image for font" << _family;
+        return;
+    }
+
+    _glyphs.clear();
+    glm::vec2 imageSize = toGlm(image.size());
+    _distanceRange /= imageSize;
+    foreach(Glyph g, glyphs) {
+        // Adjust the pixel texture coordinates into UV coordinates,
+        g.texSize /= imageSize;
+        g.texOffset /= imageSize;
+        // Y flip
+        g.texOffset.y = 1.0f - (g.texOffset.y + g.texSize.y);
+        g.offset.y = -(1.0f - (g.offset.y + g.size.y));
+        // store in the character to glyph hash
+        _glyphs[g.c] = g;
+    };
+
+    image = image.convertToFormat(QImage::Format_RGBA8888);
+
+    gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB);
+    gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB);
+    if (image.hasAlphaChannel()) {
+        formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA);
+        formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::BGRA);
+    }
+    // FIXME: We're forcing this to use only one mip, and then manually doing anisotropic filtering in the shader,
+    // and also calling textureLod.  Shouldn't this just use anisotropic filtering and auto-generate mips?
+    // We should also use smoothstep for anti-aliasing, as explained here: https://github.com/libgdx/libgdx/wiki/Distance-field-fonts
+    _texture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::SINGLE_MIP,
+                                      gpu::Sampler(gpu::Sampler::FILTER_MIN_POINT_MAG_LINEAR));
+    _texture->setStoredMipFormat(formatMip);
+    _texture->assignStoredMip(0, image.sizeInBytes(), image.constBits());
+    _texture->setImportant(true);
+
+    _loaded = true;
+    _needsParamsUpdate = true;
+}
+
 static QHash<QString, Font::Pointer> LOADED_FONTS;
 
 Font::Pointer Font::load(const QString& family) {
@@ -91,16 +233,15 @@ Font::Pointer Font::load(const QString& family) {
         QString loadFilename;
 
         if (family == ROBOTO_FONT_FAMILY) {
-            loadFilename = ":/Roboto.sdff";
+            loadFilename = ":/Roboto.arfont";
         } else if (family == INCONSOLATA_FONT_FAMILY) {
-            loadFilename = ":/InconsolataMedium.sdff";
+            loadFilename = ":/InconsolataMedium.arfont";
         } else if (family == COURIER_FONT_FAMILY) {
-            loadFilename = ":/CourierPrime.sdff";
+            loadFilename = ":/CourierPrime.arfont";
         } else if (family == TIMELESS_FONT_FAMILY) {
-            loadFilename = ":/Timeless.sdff";
+            loadFilename = ":/Timeless.arfont";
         } else if (family.startsWith("http")) {
-            auto loadingFont = std::make_shared<Font>();
-            loadingFont->setLoaded(false);
+            auto loadingFont = std::make_shared<Font>(family);
             LOADED_FONTS[family] = loadingFont;
 
             auto& networkAccessManager = NetworkAccessManager::getInstance();
@@ -114,7 +255,7 @@ Font::Pointer Font::load(const QString& family) {
             connect(networkReply, &QNetworkReply::finished, loadingFont.get(), &Font::handleFontNetworkReply);
         } else if (!LOADED_FONTS.contains(ROBOTO_FONT_FAMILY)) {
             // Unrecognized font and we haven't loaded Roboto yet
-            loadFilename = ":/Roboto.sdff";
+            loadFilename = ":/Roboto.arfont";
         } else {
             // Unrecognized font but we've already loaded Roboto
             LOADED_FONTS[family] = LOADED_FONTS[ROBOTO_FONT_FAMILY];
@@ -126,25 +267,13 @@ Font::Pointer Font::load(const QString& family) {
 
             qCDebug(renderutils) << "Loaded font" << loadFilename << "from Qt Resource System.";
 
-            LOADED_FONTS[family] = load(fontFile);
+            LOADED_FONTS[family] = load(family, fontFile);
         }
     }
     return LOADED_FONTS[family];
 }
 
-void Font::handleFontNetworkReply() {
-    auto requestReply = qobject_cast<QNetworkReply*>(sender());
-    Q_ASSERT(requestReply != nullptr);
-
-    if (requestReply->error() == QNetworkReply::NoError) {
-        setLoaded(true);
-        read(*requestReply);
-    } else {
-        qDebug() << "Error downloading " << requestReply->url() << " - " << requestReply->errorString();
-    }
-}
-
-Font::Font() {
+Font::Font(const QString& family) : _family(family) {
     static std::once_flag once;
     std::call_once(once, []{
         Q_INIT_RESOURCE(fonts);
@@ -197,82 +326,6 @@ glm::vec2 Font::computeExtent(const QString& str) const {
     return extent;
 }
 
-void Font::read(QIODevice& in) {
-    uint8_t header[4];
-    readStream(in, header);
-    if (memcmp(header, "SDFF", 4)) {
-        qDebug() << "Bad SDFF file";
-        _loaded = false;
-        return;
-    }
-
-    uint16_t version;
-    readStream(in, version);
-
-    // read font name
-    _family = "";
-    if (version > 0x0001) {
-        char c;
-        readStream(in, c);
-        while (c) {
-            _family += c;
-            readStream(in, c);
-        }
-    }
-
-    // read font data
-    readStream(in, _leading);
-    readStream(in, _ascent);
-    readStream(in, _descent);
-    readStream(in, _spaceWidth);
-    _fontSize = _ascent + _descent;
-
-    // Read character count
-    uint16_t count;
-    readStream(in, count);
-    // read metrics data for each character
-    QVector<Glyph> glyphs(count);
-    // std::for_each instead of Qt foreach because we need non-const references
-    std::for_each(glyphs.begin(), glyphs.end(), [&](Glyph& g) {
-        g.read(in);
-    });
-
-    // read image data
-    QImage image;
-    if (!image.loadFromData(in.readAll(), "PNG")) {
-        qDebug() << "Failed to read SDFF image";
-        _loaded = false;
-        return;
-    }
-
-    _glyphs.clear();
-    glm::vec2 imageSize = toGlm(image.size());
-    foreach(Glyph g, glyphs) {
-        // Adjust the pixel texture coordinates into UV coordinates,
-        g.texSize /= imageSize;
-        g.texOffset /= imageSize;
-        // store in the character to glyph hash
-        _glyphs[g.c] = g;
-    };
-
-    image = image.convertToFormat(QImage::Format_RGBA8888);
-
-    gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB);
-    gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB);
-    if (image.hasAlphaChannel()) {
-        formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA);
-        formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::BGRA);
-    }
-    // FIXME: We're forcing this to use only one mip, and then manually doing anisotropic filtering in the shader,
-    // and also calling textureLod.  Shouldn't this just use anisotropic filtering and auto-generate mips?
-    // We should also use smoothstep for anti-aliasing, as explained here: https://github.com/libgdx/libgdx/wiki/Distance-field-fonts
-    _texture = gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::SINGLE_MIP,
-                                      gpu::Sampler(gpu::Sampler::FILTER_MIN_POINT_MAG_LINEAR));
-    _texture->setStoredMipFormat(formatMip);
-    _texture->assignStoredMip(0, image.sizeInBytes(), image.constBits());
-    _texture->setImportant(true);
-}
-
 void Font::setupGPU() {
     if (_pipelines.empty()) {
         using namespace shader::render_utils::program;
@@ -340,13 +393,21 @@ void Font::buildVertices(Font::DrawInfo& drawInfo, const QString& str, const glm
     drawInfo.bounds = bounds;
     drawInfo.origin = origin;
 
-    float enlargedBoundsX = bounds.x - 0.5f * DOUBLE_MAX_OFFSET_PIXELS * float(enlargeForShadows);
-    float rightEdge = origin.x + enlargedBoundsX;
+    float rightEdge = origin.x + bounds.x;
 
     // Top left of text
+    bool firstTokenOfLine = true;
     glm::vec2 advance = origin;
     std::vector<std::pair<Glyph, vec2>> glyphsAndCorners;
-    foreach(const QString& token, tokenizeForWrapping(str)) {
+    const QStringList tokens = tokenizeForWrapping(str);
+    for (int i = 0; i < tokens.length(); i++) {
+        const QString& token = tokens[i];
+
+        if ((bounds.y != -1) && (advance.y < origin.y - bounds.y)) {
+            // We are out of the y bound, stop drawing
+            break;
+        }
+
         bool isNewLine = (token == QString('\n'));
         bool forceNewLine = false;
 
@@ -355,43 +416,50 @@ void Font::buildVertices(Font::DrawInfo& drawInfo, const QString& str, const glm
             // We are out of the x bound, force new line
             forceNewLine = true;
         }
-        if (isNewLine || forceNewLine) {
+
+        if (isNewLine || (forceNewLine && !firstTokenOfLine)) {
+            if (forceNewLine && !firstTokenOfLine) {
+                // We want to try this token again on the new line
+                i--;
+            }
+
             // Character return, move the advance to a new line
             advance = glm::vec2(origin.x, advance.y - _leading);
-
-            if (isNewLine) {
-                // No need to draw anything, go directly to next token
-                continue;
-            } else if (computeExtent(token).x > enlargedBoundsX) {
-                // token will never fit, stop drawing
-                break;
-            }
-        }
-        if ((bounds.y != -1) && (advance.y - _fontSize < origin.y - bounds.y)) {
-            // We are out of the y bound, stop drawing
-            break;
+            firstTokenOfLine = true;
+            // No need to draw anything, go directly to next token
+            continue;
         }
 
         // Draw the token
-        if (!isNewLine) {
-            for (auto c : token) {
-                auto glyph = _glyphs[c];
-
-                glyphsAndCorners.emplace_back(glyph, advance - glm::vec2(0.0f, _ascent));
-
-                // Advance by glyph size
-                advance.x += glyph.d;
+        for (const QChar& c : token) {
+            if (advance.x > rightEdge) {
+                break;
             }
+            const Glyph& glyph = _glyphs[c];
 
+            glyphsAndCorners.emplace_back(glyph, advance);
+
+            // Advance by glyph size
+            advance.x += glyph.d;
+        }
+
+        if (forceNewLine && firstTokenOfLine) {
+            // If the first word of a line didn't fit, we draw as many characters as we could, now go to the next line
+            // Character return, move the advance to a new line
+            advance = glm::vec2(origin.x, advance.y - _leading);
+            firstTokenOfLine = true;
+        } else {
             // Add space after all non return tokens
             advance.x += _spaceWidth;
+            // Our token fits in the x direction!  Any subsequent tokens won't be the first for this line.
+            firstTokenOfLine = false;
         }
     }
 
     std::vector<QuadBuilder> quadBuilders;
     quadBuilders.reserve(glyphsAndCorners.size());
     {
-        int i = glyphsAndCorners.size() - 1;
+        int i = (int)glyphsAndCorners.size() - 1;
         while (i >= 0) {
             auto nextGlyphAndCorner = glyphsAndCorners[i];
             float rightSpacing = rightEdge - (nextGlyphAndCorner.second.x + nextGlyphAndCorner.first.d);
@@ -399,7 +467,7 @@ void Font::buildVertices(Font::DrawInfo& drawInfo, const QString& str, const glm
                                                                        alignment, rightSpacing));
             i--;
             while (i >= 0) {
-                auto prevGlyphAndCorner = glyphsAndCorners[i];
+                const auto& prevGlyphAndCorner = glyphsAndCorners[i];
                 // We're to the right of the last character we checked, which means we're on a previous line, so we need to
                 // recalculate the spacing, so we exit this loop
                 if (prevGlyphAndCorner.second.x >= nextGlyphAndCorner.second.x) {
@@ -416,7 +484,7 @@ void Font::buildVertices(Font::DrawInfo& drawInfo, const QString& str, const glm
     }
 
     // The quadBuilders is backwards now because we looped over the glyphs backwards to adjust their alignment
-    for (int i = quadBuilders.size() - 1; i >= 0; i--) {
+    for (int i = (int)quadBuilders.size() - 1; i >= 0; i--) {
         quint16 verticesOffset = numVertices;
         drawInfo.verticesBuffer->append(quadBuilders[i]);
         numVertices += VERTICES_PER_QUAD;
@@ -458,8 +526,10 @@ void Font::drawString(gpu::Batch& batch, Font::DrawInfo& drawInfo, const DrawPro
     int textEffect = (int)props.effect;
     const int SHADOW_EFFECT = (int)TextEffect::SHADOW_EFFECT;
 
+    const bool boundsChanged = props.bounds != drawInfo.bounds || props.origin != drawInfo.origin;
+
     // If we're switching to or from shadow effect mode, we need to rebuild the vertices
-    if (props.str != drawInfo.string || props.bounds != drawInfo.bounds || props.origin != drawInfo.origin || props.alignment != _alignment ||
+    if (props.str != drawInfo.string || boundsChanged || props.alignment != _alignment ||
             (drawInfo.params.effect != textEffect && (textEffect == SHADOW_EFFECT || drawInfo.params.effect == SHADOW_EFFECT)) ||
             (textEffect == SHADOW_EFFECT && props.scale != _scale)) {
         _scale = props.scale;
@@ -469,8 +539,9 @@ void Font::drawString(gpu::Batch& batch, Font::DrawInfo& drawInfo, const DrawPro
 
     setupGPU();
 
-    if (!drawInfo.paramsBuffer || drawInfo.params.color != props.color || drawInfo.params.effectColor != props.effectColor ||
-        drawInfo.params.effectThickness != props.effectThickness || drawInfo.params.effect != textEffect) {
+    if (!drawInfo.paramsBuffer || boundsChanged || _needsParamsUpdate || drawInfo.params.color != props.color ||
+            drawInfo.params.effectColor != props.effectColor || drawInfo.params.effectThickness != props.effectThickness ||
+            drawInfo.params.effect != textEffect) {
         drawInfo.params.color = props.color;
         drawInfo.params.effectColor = props.effectColor;
         drawInfo.params.effectThickness = props.effectThickness;
@@ -478,14 +549,18 @@ void Font::drawString(gpu::Batch& batch, Font::DrawInfo& drawInfo, const DrawPro
 
         // need the gamma corrected color here
         DrawParams gpuDrawParams;
+        gpuDrawParams.bounds = glm::vec4(props.origin, props.bounds);
         gpuDrawParams.color = ColorUtils::sRGBToLinearVec4(drawInfo.params.color);
-        gpuDrawParams.effectColor = ColorUtils::sRGBToLinearVec3(drawInfo.params.effectColor);
-        gpuDrawParams.effectThickness = drawInfo.params.effectThickness;
+        gpuDrawParams.unitRange = _distanceRange;
         gpuDrawParams.effect = drawInfo.params.effect;
+        gpuDrawParams.effectThickness = drawInfo.params.effectThickness;
+        gpuDrawParams.effectColor = ColorUtils::sRGBToLinearVec3(drawInfo.params.effectColor);
         if (!drawInfo.paramsBuffer) {
             drawInfo.paramsBuffer = std::make_shared<gpu::Buffer>(sizeof(DrawParams), nullptr);
         }
         drawInfo.paramsBuffer->setSubData(0, sizeof(DrawParams), (const gpu::Byte*)&gpuDrawParams);
+
+        _needsParamsUpdate = false;
     }
 
     batch.setPipeline(_pipelines[std::make_tuple(props.color.a < 1.0f, props.unlit, props.forward, props.mirror)]);
diff --git a/libraries/render-utils/src/text/Font.h b/libraries/render-utils/src/text/Font.h
index e8a353a686..c9d96bc6f6 100644
--- a/libraries/render-utils/src/text/Font.h
+++ b/libraries/render-utils/src/text/Font.h
@@ -24,22 +24,25 @@ class Font : public QObject {
 public:
     using Pointer = std::shared_ptr<Font>;
 
-    Font();
+    Font(const QString& family);
 
     void read(QIODevice& path);
 
     struct DrawParams {
-        vec4 color { 0 };
+        vec4 bounds { 0.0f };
+        vec4 color { 0.0f };
 
-        vec3 effectColor { 0 };
-        float effectThickness { 0 };
+        vec2 unitRange { 1.0f };
 
         int effect { 0 };
+        float effectThickness { 0.0f };
+
+        vec3 effectColor { 0.0f };
 
 #if defined(__clang__)
         __attribute__((unused))
 #endif
-        vec3 _spare;
+        float _spare;
     };
 
     struct DrawInfo {
@@ -85,13 +88,12 @@ public:
     static Pointer load(const QString& family);
 
     bool isLoaded() const { return _loaded; }
-    void setLoaded(bool loaded) { _loaded = loaded; }
 
 public slots:
     void handleFontNetworkReply();
 
 private:
-    static Pointer load(QIODevice& fontFile);
+    static Pointer load(const QString& family, QIODevice& fontFile);
     QStringList tokenizeForWrapping(const QString& str) const;
     QStringList splitLines(const QString& str) const;
     glm::vec2 computeTokenExtent(const QString& str) const;
@@ -111,16 +113,16 @@ private:
 
     // Font characteristics
     QString _family;
+    glm::vec2 _distanceRange { 1.0f };
     float _fontSize { 0.0f };
     float _leading { 0.0f };
-    float _ascent { 0.0f };
-    float _descent { 0.0f };
     float _spaceWidth { 0.0f };
 
     float _scale { 0.0f };
-    TextAlignment _alignment;
+    TextAlignment _alignment { TextAlignment::LEFT };
 
-    bool _loaded { true };
+    bool _loaded { false };
+    bool _needsParamsUpdate { false };
 
     gpu::TexturePointer _texture;
     gpu::BufferStreamPointer _stream;
diff --git a/libraries/render-utils/src/text/Glyph.cpp b/libraries/render-utils/src/text/Glyph.cpp
index ee3656bc00..c54f0422f3 100644
--- a/libraries/render-utils/src/text/Glyph.cpp
+++ b/libraries/render-utils/src/text/Glyph.cpp
@@ -9,15 +9,3 @@ QRectF Glyph::bounds() const {
 QRectF Glyph::textureBounds() const {
     return glmToRect(texOffset, texSize);
 }
-
-void Glyph::read(QIODevice& in) {
-    uint16_t charcode;
-    readStream(in, charcode);
-    c = charcode;
-    readStream(in, texOffset);
-    readStream(in, size);
-    readStream(in, offset);
-    readStream(in, d);
-    // texSize is divided by the image size later
-    texSize = size;
-}
diff --git a/libraries/render-utils/src/text/Glyph.h b/libraries/render-utils/src/text/Glyph.h
index 3cb08cc7e2..5aeb96b2c6 100644
--- a/libraries/render-utils/src/text/Glyph.h
+++ b/libraries/render-utils/src/text/Glyph.h
@@ -26,13 +26,10 @@ struct Glyph {
     vec2 size;
     vec2 offset;
     float d;  // xadvance - adjusts character positioning
-    size_t indexOffset;
 
     // We adjust bounds because offset is the bottom left corner of the font but the top left corner of a QRect
     QRectF bounds() const;
     QRectF textureBounds() const;
-
-    void read(QIODevice& in);
 };
 
 #endif
diff --git a/libraries/render/src/render/FilterTask.cpp b/libraries/render/src/render/FilterTask.cpp
index 90720f5666..22f61a42fe 100644
--- a/libraries/render/src/render/FilterTask.cpp
+++ b/libraries/render/src/render/FilterTask.cpp
@@ -139,12 +139,12 @@ void IDsToBounds::run(const RenderContextPointer& renderContext, const ItemIDs&
         for (auto id : inItems) {
             auto& item = scene->getItem(id);
             if (item.exist()) {
-                outItems.emplace_back(ItemBound{ id, item.getBound(renderContext->args) });
+                outItems.emplace_back(ItemBound(id, item.getBound(renderContext->args)));
             }
         }
     } else {
         for (auto id : inItems) {
-            outItems.emplace_back(ItemBound{ id });
+            outItems.emplace_back(ItemBound(id));
         }
     }
 }
diff --git a/libraries/render/src/render/HighlightStyle.h b/libraries/render/src/render/HighlightStyle.h
index 8bef5c33c3..138674ffbb 100644
--- a/libraries/render/src/render/HighlightStyle.h
+++ b/libraries/render/src/render/HighlightStyle.h
@@ -3,6 +3,7 @@
 
 //  Created by Olivier Prat on 11/06/2017.
 //  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -12,6 +13,7 @@
 #define hifi_render_utils_HighlightStyle_h
 
 #include <glm/vec3.hpp>
+#include <glm/gtx/string_cast.hpp>
 
 #include <string>
 
@@ -21,23 +23,59 @@ namespace render {
     class HighlightStyle {
     public:
         struct RGBA {
-            glm::vec3 color{ 1.0f, 0.7f, 0.2f };
-            float alpha{ 0.9f };
+            glm::vec3 color { 1.0f, 0.7f, 0.2f };
+            float alpha { 0.9f };
 
             RGBA(const glm::vec3& c, float a) : color(c), alpha(a) {}
+
+            std::string toString() const { return glm::to_string(color) + " " + std::to_string(alpha); }
         };
 
-        RGBA _outlineUnoccluded{ { 1.0f, 0.7f, 0.2f }, 0.9f };
-        RGBA _outlineOccluded{ { 1.0f, 0.7f, 0.2f }, 0.9f };
-        RGBA _fillUnoccluded{ { 0.2f, 0.7f, 1.0f }, 0.0f };
-        RGBA _fillOccluded{ { 0.2f, 0.7f, 1.0f }, 0.0f };
+        RGBA _outlineUnoccluded { { 1.0f, 0.7f, 0.2f }, 0.9f };
+        RGBA _outlineOccluded { { 1.0f, 0.7f, 0.2f }, 0.9f };
+        RGBA _fillUnoccluded { { 0.2f, 0.7f, 1.0f }, 0.0f };
+        RGBA _fillOccluded { { 0.2f, 0.7f, 1.0f }, 0.0f };
 
-        float _outlineWidth{ 2.0f };
-        bool _isOutlineSmooth{ false };
+        float _outlineWidth { 2.0f };
+        bool _isOutlineSmooth { false };
 
         bool isFilled() const {
             return _fillUnoccluded.alpha > 5e-3f || _fillOccluded.alpha > 5e-3f;
         }
+
+        std::string toString() const {
+            return _outlineUnoccluded.toString() + _outlineOccluded.toString() + _fillUnoccluded.toString() +
+                   _fillOccluded.toString() + std::to_string(_outlineWidth) + std::to_string(_isOutlineSmooth);
+        }
+
+        static HighlightStyle calculateOutlineStyle(uint8_t mode, float outlineWidth, const glm::vec3& outline,
+                const glm::vec3& position, const ViewFrustum& viewFrustum, size_t screenHeight) {
+            HighlightStyle style;
+            style._outlineUnoccluded.color = outline;
+            style._outlineUnoccluded.alpha = 1.0f;
+            style._outlineOccluded.alpha = 0.0f;
+            style._fillUnoccluded.alpha = 0.0f;
+            style._fillOccluded.alpha = 0.0f;
+            style._isOutlineSmooth = false;
+
+            if (mode == 1) { // OUTLINE_WORLD
+                // FIXME: this is a hacky approximation, which gives us somewhat accurate widths with distance based falloff.
+                // Our outline implementation doesn't support the necessary vertex based extrusion to do real world based outlines.
+                glm::vec4 viewPos = glm::inverse(viewFrustum.getView()) * glm::vec4(position, 1.0f);
+
+                const glm::mat4& projection = viewFrustum.getProjection();
+                glm::vec4 p1 = projection * (viewPos + glm::vec4(0.0f, 0.5f * outlineWidth, 0.0f, 0.0f));
+                p1 /= p1.w;
+                glm::vec4 p2 = projection * (viewPos - glm::vec4(0.0f, 0.5f * outlineWidth, 0.0f, 0.0f));
+                p2 /= p2.w;
+
+                style._outlineWidth = floor(0.5f * (float)screenHeight * fabs(p1.y - p2.y));
+            } else { // OUTLINE_SCREEN
+                style._outlineWidth = floor(outlineWidth * (float)screenHeight);
+            }
+
+            return style;
+        }
     };
 
 }
diff --git a/libraries/render/src/render/Item.cpp b/libraries/render/src/render/Item.cpp
index f169de5c98..b5a6befdcb 100644
--- a/libraries/render/src/render/Item.cpp
+++ b/libraries/render/src/render/Item.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 1/26/16.
 //  Copyright 2014 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -174,4 +175,11 @@ namespace render {
         }
         return payload->computeMirrorView(viewFrustum);
     }
+
+    template <> HighlightStyle payloadGetOutlineStyle(const PayloadProxyInterface::Pointer& payload, const ViewFrustum& viewFrustum, const size_t height) {
+        if (!payload) {
+            return HighlightStyle();
+        }
+        return payload->getOutlineStyle(viewFrustum, height);
+    }
 }
diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h
index a5cda7cedf..791b180e04 100644
--- a/libraries/render/src/render/Item.h
+++ b/libraries/render/src/render/Item.h
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 1/26/16.
 //  Copyright 2014 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -29,6 +30,7 @@
 #include "ShapePipeline.h"
 
 #include "BlendshapeConstants.h"
+#include "HighlightStyle.h"
 
 namespace render {
 
@@ -104,18 +106,19 @@ public:
         META_CULL_GROUP,  // As a meta item, the culling of my sub items is based solely on my bounding box and my visibility in the view
         SUB_META_CULLED,  // As a sub item of a meta render item set as cull group, need to be set to my culling to the meta render it
 
-        FIRST_TAG_BIT, // 8 Tags available to organize the items and filter them against
+        FIRST_TAG_BIT,    // 8 Tags available to organize the items and filter them against
         LAST_TAG_BIT = FIRST_TAG_BIT + NUM_TAGS,
 
-        FIRST_LAYER_BIT, // 8 Exclusive Layers (encoded in 3 bits) available to organize the items in layers, an item can only belong to ONE layer
+        FIRST_LAYER_BIT,  // 8 Exclusive Layers (encoded in 3 bits) available to organize the items in layers, an item can only belong to ONE layer
         LAST_LAYER_BIT = FIRST_LAYER_BIT + NUM_LAYER_BITS,
 
-        MIRROR,
-        SIMULATE,
+        MIRROR,           // Item is a mirror
+        SIMULATE,         // Item requires simulation
+        OUTLINE,          // Item has an outline
 
         __SMALLER,        // Reserved bit for spatialized item to indicate that it is smaller than expected in the cell in which it belongs (probably because it overlaps over several smaller cells)
 
-        NUM_FLAGS,      // Not a valid flag
+        NUM_FLAGS,        // Not a valid flag
     };
     typedef std::bitset<NUM_FLAGS> Flags;
 
@@ -175,6 +178,9 @@ public:
         Builder& withLayer(uint8_t layer) { _flags = evalLayerBitsWithKeyBits(layer, _flags.to_ulong()); return (*this); }
         Builder& withoutLayer() { return withLayer(LAYER_DEFAULT); }
 
+        Builder& withOutline() { _flags.set(OUTLINE); return (*this); }
+        Builder& withoutOutline() { _flags.reset(OUTLINE); return (*this); }
+
         // Convenient standard keys that we will keep on using all over the place
         static Builder opaqueShape() { return Builder().withTypeShape(); }
         static Builder transparentShape() { return Builder().withTypeShape().withTransparent(); }
@@ -224,6 +230,8 @@ public:
     bool isLayered() const { return getLayer() != LAYER_DEFAULT; }
     bool isSpatial() const { return !isLayered(); }
 
+    bool isOutline() const { return _flags[OUTLINE]; }
+
     // Probably not public, flags used by the scene
     bool isSmall() const { return _flags[__SMALLER]; }
     void setSmaller(bool smaller) { (smaller ? _flags.set(__SMALLER) : _flags.reset(__SMALLER)); }
@@ -301,6 +309,9 @@ public:
         Builder& withoutLayered() { _value = ItemKey::evalLayerBitsWithKeyBits(ItemKey::LAYER_DEFAULT, _value.to_ulong()); _mask |= ItemKey::KEY_LAYER_BITS_MASK; return (*this); }
         Builder& withLayer(uint8_t layer) { _value = ItemKey::evalLayerBitsWithKeyBits(layer, _value.to_ulong()); _mask |= ItemKey::KEY_LAYER_BITS_MASK; return (*this); }
 
+        Builder& withoutOutline() { _value.reset(ItemKey::OUTLINE); _mask.set(ItemKey::OUTLINE); return (*this); }
+        Builder& withOutline() { _value.set(ItemKey::OUTLINE);  _mask.set(ItemKey::OUTLINE); return (*this); }
+
         Builder& withNothing()          { _value.reset(); _mask.reset(); return (*this); }
 
         // Convenient standard keys that we will keep on using all over the place
@@ -345,9 +356,9 @@ class ItemBound {
         ItemBound(ItemID id) : id(id) { }
         ItemBound(ItemID id, const AABox& bound) : id(id), bound(bound) { }
 
-        ItemID id;
+        ItemID id { 0 };
         AABox bound;
-        uint32_t padding;
+        uint32_t padding { 0 };
 };
 
 // many Item Bounds in a vector
@@ -461,6 +472,8 @@ public:
 
         virtual ItemID computeMirrorView(ViewFrustum& viewFrustum) const = 0;
 
+        virtual HighlightStyle getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const = 0;
+
         ~PayloadInterface() {}
 
         // Status interface is local to the base class
@@ -519,6 +532,8 @@ public:
 
     ItemID computeMirrorView(ViewFrustum& viewFrustum) const { return _payload->computeMirrorView(viewFrustum); }
 
+    HighlightStyle getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const { return _payload->getOutlineStyle(viewFrustum, height); }
+
     // Access the status
     const StatusPointer& getStatus() const { return _payload->getStatus(); }
 
@@ -577,6 +592,12 @@ template <class T> bool payloadPassesZoneOcclusionTest(const std::shared_ptr<T>&
 // Mirror Interface
 template <class T> ItemID payloadComputeMirrorView(const std::shared_ptr<T>& payloadData, ViewFrustum& viewFrustum) { return Item::INVALID_ITEM_ID; }
 
+// Outline Interface
+// Allows payloads to specify an outline style
+template <class T> HighlightStyle payloadGetOutlineStyle(const std::shared_ptr<T>& payloadData, const ViewFrustum& viewFrustum, const size_t height) {
+    return HighlightStyle();
+}
+
 // THe Payload class is the real Payload to be used
 // THis allow anything to be turned into a Payload as long as the required interface functions are available
 // When creating a new kind of payload from a new "stuff" class then you need to create specialized version for "stuff"
@@ -606,6 +627,8 @@ public:
 
     virtual ItemID computeMirrorView(ViewFrustum& viewFrustum) const override { return payloadComputeMirrorView<T>(_data, viewFrustum); }
 
+    virtual HighlightStyle getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const override { return payloadGetOutlineStyle<T>(_data, viewFrustum, height); }
+
 protected:
     DataPointer _data;
 
@@ -663,6 +686,7 @@ public:
     virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) const = 0;
     virtual bool passesZoneOcclusionTest(const std::unordered_set<QUuid>& containingZones) const = 0;
     virtual ItemID computeMirrorView(ViewFrustum& viewFrustum) const = 0;
+    virtual HighlightStyle getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const = 0;
 
     // FIXME: this isn't the best place for this since it's only used for ModelEntities, but currently all Entities use PayloadProxyInterface
     virtual void handleBlendedVertices(int blendshapeNumber, const QVector<BlendshapeOffset>& blendshapeOffsets,
@@ -677,6 +701,7 @@ template <> uint32_t metaFetchMetaSubItems(const PayloadProxyInterface::Pointer&
 template <> const ShapeKey shapeGetShapeKey(const PayloadProxyInterface::Pointer& payload);
 template <> bool payloadPassesZoneOcclusionTest(const PayloadProxyInterface::Pointer& payload, const std::unordered_set<QUuid>& containingZones);
 template <> ItemID payloadComputeMirrorView(const PayloadProxyInterface::Pointer& payload, ViewFrustum& viewFrustum);
+template <> HighlightStyle payloadGetOutlineStyle(const PayloadProxyInterface::Pointer& payload, const ViewFrustum& viewFrustum, const size_t height);
 
 typedef Item::PayloadPointer PayloadPointer;
 typedef std::vector<PayloadPointer> Payloads;
diff --git a/libraries/render/src/render/RenderFetchCullSortTask.cpp b/libraries/render/src/render/RenderFetchCullSortTask.cpp
index 4619c30de4..e8954f4161 100644
--- a/libraries/render/src/render/RenderFetchCullSortTask.cpp
+++ b/libraries/render/src/render/RenderFetchCullSortTask.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Zach Pomerantz on 12/22/2016.
 //  Copyright 2016 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -35,7 +36,7 @@ void RenderFetchCullSortTask::build(JobModel& task, const Varying& input, Varyin
     const auto nonspatialSelection = task.addJob<FetchNonspatialItems>("FetchLayeredSelection", nonspatialFilter);
 
     // Multi filter visible items into different buckets
-    const int NUM_SPATIAL_FILTERS = 6;
+    const int NUM_SPATIAL_FILTERS = 7;
     const int NUM_NON_SPATIAL_FILTERS = 4;
     const int OPAQUE_SHAPE_BUCKET = 0;
     const int TRANSPARENT_SHAPE_BUCKET = 1;
@@ -43,6 +44,7 @@ void RenderFetchCullSortTask::build(JobModel& task, const Varying& input, Varyin
     const int LIGHT_BUCKET = 3;
     const int META_BUCKET = 4;
     const int MIRROR_BUCKET = 5;
+    const int OUTLINE_BUCKET = 6;
     const int BACKGROUND_BUCKET = 3;
     MultiFilterItems<NUM_SPATIAL_FILTERS>::ItemFilterArray spatialFilters = { {
             ItemFilter::Builder::opaqueShape().withoutMirror(),
@@ -50,7 +52,8 @@ void RenderFetchCullSortTask::build(JobModel& task, const Varying& input, Varyin
             ItemFilter::Builder().withSimulate(),
             ItemFilter::Builder::light(),
             ItemFilter::Builder::meta().withoutMirror(),
-            ItemFilter::Builder::mirror()
+            ItemFilter::Builder::mirror(),
+            ItemFilter::Builder().withVisible().withOutline()
         } };
     MultiFilterItems<NUM_NON_SPATIAL_FILTERS>::ItemFilterArray nonspatialFilters = { {
             ItemFilter::Builder::opaqueShape(),
@@ -86,7 +89,7 @@ void RenderFetchCullSortTask::build(JobModel& task, const Varying& input, Varyin
 
     task.addJob<ClearContainingZones>("ClearContainingZones");
 
-    output = Output(BucketList{ opaques, transparents, lights, metas, mirrors, simulate,
+    output = Output(BucketList{ opaques, transparents, lights, metas, mirrors, simulate, filteredSpatialBuckets[OUTLINE_BUCKET],
                     filteredLayeredOpaque.getN<FilterLayeredItems::Outputs>(0), filteredLayeredTransparent.getN<FilterLayeredItems::Outputs>(0),
                     filteredLayeredOpaque.getN<FilterLayeredItems::Outputs>(1), filteredLayeredTransparent.getN<FilterLayeredItems::Outputs>(1),
                     background }, spatialSelection);
diff --git a/libraries/render/src/render/RenderFetchCullSortTask.h b/libraries/render/src/render/RenderFetchCullSortTask.h
index ffbd815167..5802245b90 100644
--- a/libraries/render/src/render/RenderFetchCullSortTask.h
+++ b/libraries/render/src/render/RenderFetchCullSortTask.h
@@ -4,6 +4,7 @@
 //
 //  Created by Zach Pomerantz on 12/22/2016.
 //  Copyright 2016 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -25,6 +26,7 @@ public:
         META,
         MIRROR,
         SIMULATE,
+        OUTLINE,
         LAYER_FRONT_OPAQUE_SHAPE,
         LAYER_FRONT_TRANSPARENT_SHAPE,
         LAYER_HUD_OPAQUE_SHAPE,
diff --git a/libraries/render/src/render/Scene.cpp b/libraries/render/src/render/Scene.cpp
index 5500183196..f09a646ce6 100644
--- a/libraries/render/src/render/Scene.cpp
+++ b/libraries/render/src/render/Scene.cpp
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 1/11/15.
 //  Copyright 2014 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -608,6 +609,22 @@ void Scene::resetSelections(const Transaction::SelectionResets& transactions) {
     }
 }
 
+void Scene::addItemToSelection(const std::string& selectionName, ItemID itemID) {
+    std::unique_lock<std::mutex> lock(_selectionsMutex);
+    auto found = _selections.find(selectionName);
+    if (found == _selections.end()) {
+        Selection selection = Selection(selectionName, { itemID });
+        _selections[selectionName] = selection;
+    } else {
+        _selections[selectionName].add(itemID);
+    }
+}
+
+void Scene::removeSelection(const std::string& selectionName) {
+    std::unique_lock<std::mutex> lock(_selectionsMutex);
+    _selections.erase(selectionName);
+}
+
 // Access a particular Stage (empty if doesn't exist)
 // Thread safe
 StagePointer Scene::getStage(const Stage::Name& name) const {
diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h
index 4dbcaa4e8f..fb88fbfdce 100644
--- a/libraries/render/src/render/Scene.h
+++ b/libraries/render/src/render/Scene.h
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 1/11/15.
 //  Copyright 2014 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -166,6 +167,14 @@ public:
     // Thread safe
     bool isSelectionEmpty(const Selection::Name& name) const;
 
+    // Add a single item to a selection by name
+    // Thread safe
+    void addItemToSelection(const std::string& selectionName, ItemID itemID);
+
+    // Remove a selection by name
+    // Thread safe
+    void removeSelection(const std::string& selectionName);
+
     // This next call are  NOT threadsafe, you have to call them from the correct thread to avoid any potential issues
 
     // Access a particular item from its ID
@@ -196,6 +205,8 @@ public:
 
     void simulate(ItemID id, RenderArgs* args) { _items[id].renderSimulate(args); }
 
+    HighlightStyle getOutlineStyle(ItemID id, const ViewFrustum& viewFrustum, uint16_t height) { return _items[id].getOutlineStyle(viewFrustum, height); }
+
 protected:
 
     // Thread safe elements that can be accessed from anywhere
diff --git a/libraries/render/src/render/Selection.h b/libraries/render/src/render/Selection.h
index 05b2395b42..0e3ef0eb77 100644
--- a/libraries/render/src/render/Selection.h
+++ b/libraries/render/src/render/Selection.h
@@ -4,6 +4,7 @@
 //
 //  Created by Sam Gateau on 4/4/2017.
 //  Copyright 2017 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -37,7 +38,8 @@ namespace render {
 
         // Test if the ID is in the selection, return the index or -1 if not present
         static const int NOT_FOUND{ -1 };
-                
+
+        void add(ItemID id) { _items.push_back(id); }
         int find(ItemID id) const;
         bool contains(ItemID id) const { return find(id) > NOT_FOUND; }
 
diff --git a/libraries/render/src/render/ShapePipeline.cpp b/libraries/render/src/render/ShapePipeline.cpp
index 048e08e959..4d1682de9a 100644
--- a/libraries/render/src/render/ShapePipeline.cpp
+++ b/libraries/render/src/render/ShapePipeline.cpp
@@ -60,8 +60,8 @@ ShapeKey::Filter::Builder::Builder() {
 }
 
 void ShapePlumber::addPipelineHelper(const Filter& filter, ShapeKey key, int bit, const PipelinePointer& pipeline) const {
-    // Iterate over all keys
-    if (bit < (int)ShapeKey::FlagBit::NUM_FLAGS) {
+    // Iterate over all non-custom keys
+    if (bit < (int)ShapeKey::FlagBit::NUM_NON_CUSTOM - 1) {
         addPipelineHelper(filter, key, bit + 1, pipeline);
         if (!filter._mask[bit]) {
             // Toggle bits set as insignificant in filter._mask 
diff --git a/libraries/render/src/render/ShapePipeline.h b/libraries/render/src/render/ShapePipeline.h
index 04b9919140..fd8b729ffa 100644
--- a/libraries/render/src/render/ShapePipeline.h
+++ b/libraries/render/src/render/ShapePipeline.h
@@ -4,6 +4,7 @@
 //
 //  Created by Zach Pomerantz on 12/31/15.
 //  Copyright 2015 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -38,6 +39,7 @@ public:
         FADE,
         CULL_FACE_NONE, // if neither of these are set, we're CULL_FACE_BACK
         CULL_FACE_FRONT,
+        MTOON,
 
         OWN_PIPELINE,
         INVALID,
@@ -52,6 +54,7 @@ public:
         CUSTOM_7,
 
         NUM_FLAGS, // Not a valid flag
+        NUM_NON_CUSTOM = INVALID,
 
         CUSTOM_MASK = (0xFF << CUSTOM_0),
 
@@ -84,6 +87,7 @@ public:
         Builder& withDepthBias() { _flags.set(DEPTH_BIAS); return (*this); }
         Builder& withWireframe() { _flags.set(WIREFRAME); return (*this); }
         Builder& withFade() { _flags.set(FADE); return (*this); }
+        Builder& withMToon() { _flags.set(MTOON); return (*this); }
 
         Builder& withoutCullFace() { return withCullFaceMode(graphics::MaterialKey::CullFaceMode::CULL_NONE); }
         Builder& withCullFaceMode(graphics::MaterialKey::CullFaceMode cullFaceMode) {
@@ -109,7 +113,7 @@ public:
         Builder& withOwnPipeline() { _flags.set(OWN_PIPELINE); return (*this); }
         Builder& invalidate() { _flags.set(INVALID); return (*this); }
 
-        Builder& withCustom(uint8_t custom) {  _flags &= (~CUSTOM_MASK); _flags |= (custom << CUSTOM_0); return (*this); }
+        Builder& withCustom(uint8_t custom) { _flags &= (~CUSTOM_MASK); _flags |= (custom << CUSTOM_0); return (*this); }
         
         static const ShapeKey ownPipeline() { return Builder().withOwnPipeline(); }
         static const ShapeKey invalid() { return Builder().invalidate(); }
@@ -184,6 +188,9 @@ public:
             Builder& withFade() { _flags.set(FADE); _mask.set(FADE); return (*this); }
             Builder& withoutFade() { _flags.reset(FADE); _mask.set(FADE); return (*this); }
 
+            Builder& withMToon() { _flags.set(MTOON); _mask.set(MTOON); return (*this); }
+            Builder& withoutMToon() { _flags.reset(MTOON); _mask.set(MTOON); return (*this); }
+
             Builder& withCustom(uint8_t custom) { _flags &= (~CUSTOM_MASK); _flags |= (custom << CUSTOM_0); _mask |= (CUSTOM_MASK); return (*this); }
             Builder& withoutCustom() { _flags &= (~CUSTOM_MASK);  _mask |= (CUSTOM_MASK); return (*this); }
 
@@ -210,7 +217,10 @@ public:
     bool isDepthBiased() const { return _flags[DEPTH_BIAS]; }
     bool isWireframe() const { return _flags[WIREFRAME]; }
     bool isCullFace() const { return !_flags[CULL_FACE_NONE] && !_flags[CULL_FACE_FRONT]; }
+    bool isCullFaceNone() const { return _flags[CULL_FACE_NONE] && !_flags[CULL_FACE_FRONT]; }
+    bool isCullFaceFront() const { return !_flags[CULL_FACE_NONE] && _flags[CULL_FACE_FRONT]; }
     bool isFaded() const { return _flags[FADE]; }
+    bool isMToon() const { return _flags[MTOON]; }
 
     bool hasOwnPipeline() const { return _flags[OWN_PIPELINE]; }
     bool isValid() const { return !_flags[INVALID]; }
@@ -250,6 +260,7 @@ inline QDebug operator<<(QDebug debug, const ShapeKey& key) {
                 << "isWireframe:" << key.isWireframe()
                 << "isCullFace:" << key.isCullFace()
                 << "isFaded:" << key.isFaded()
+                << "isMToon:" << key.isMToon()
                 << "]";
         }
     } else {
diff --git a/libraries/script-engine/src/AssetScriptingInterface.cpp b/libraries/script-engine/src/AssetScriptingInterface.cpp
index dd28cf74ee..fe11582abf 100644
--- a/libraries/script-engine/src/AssetScriptingInterface.cpp
+++ b/libraries/script-engine/src/AssetScriptingInterface.cpp
@@ -74,7 +74,7 @@ void AssetScriptingInterface::uploadData(QString data, const ScriptValue& callba
     auto upload = DependencyManager::get<AssetClient>()->createUpload(dataByteArray);
 
     Promise deferred = makePromise(__FUNCTION__);
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     auto scriptEngine = engine();
     deferred->ready([=](QString error, QVariantMap result) {
         auto url = result.value("url").toString();
@@ -98,7 +98,7 @@ void AssetScriptingInterface::setMapping(QString path, QString hash, const Scrip
     auto handler = jsBindCallback(thisObject(), callback);
     auto setMappingRequest = assetClient()->createSetMappingRequest(path, hash);
     Promise deferred = makePromise(__FUNCTION__);
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     auto scriptEngine = engine();
     deferred->ready([=](QString error, QVariantMap result) {
         jsCallback(handler, scriptEngine->newValue(error), result);
@@ -136,7 +136,7 @@ void AssetScriptingInterface::downloadData(QString urlString, const ScriptValue&
     auto assetRequest = assetClient->createRequest(hash);
 
     Promise deferred = makePromise(__FUNCTION__);
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     auto scriptEngine = engine();
     deferred->ready([=](QString error, QVariantMap result) {
         // FIXME: to remain backwards-compatible the signature here is "callback(data, n/a)"
@@ -200,7 +200,7 @@ void AssetScriptingInterface::getMapping(QString asset, const ScriptValue& callb
     JS_VERIFY(AssetUtils::isValidFilePath(path), "invalid ATP file path: " + asset + "(path:"+path+")");
     JS_VERIFY(callback.isFunction(), "expected second parameter to be a callback function");
     Promise promise = getAssetInfo(path);
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     auto scriptEngine = engine();
     promise->ready([=](QString error, QVariantMap result) {
         jsCallback(handler, scriptEngine->newValue(error), scriptEngine->newValue(result.value("hash").toString()));
@@ -234,7 +234,7 @@ Promise AssetScriptingInterface::jsPromiseReady(Promise promise, const ScriptVal
     if (!jsVerify(handler.isValid(), "jsPromiseReady -- invalid callback handler")) {
         return nullptr;
     }
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     auto scriptEngine = engine();
     return promise->ready([this, handler, scriptEngine](QString error, QVariantMap result) {
         jsCallback(handler, scriptEngine->newValue(error), result);
@@ -244,7 +244,6 @@ Promise AssetScriptingInterface::jsPromiseReady(Promise promise, const ScriptVal
 void AssetScriptingInterface::jsCallback(const ScriptValue& handler,
                                          const ScriptValue& error, const ScriptValue& result) {
     Q_ASSERT(thread() == QThread::currentThread());
-    Q_ASSERT(engine);
     //V8TODO: which kind of script context guard needs to be used here?
     ScriptContextGuard scriptContextGuard(_scriptManager->engine()->currentContext());
     auto errorValue = !error.toBool() ? engine()->nullValue() : error;
@@ -546,7 +545,7 @@ void AssetScriptingInterface::loadFromCache(const ScriptValue& options, const Sc
 }
 
 bool AssetScriptingInterface::canWriteCacheValue(const QUrl& url) {
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     auto scriptManager = engine()->manager();
     if (!scriptManager) {
         return false;
diff --git a/libraries/script-engine/src/ConsoleScriptingInterface.cpp b/libraries/script-engine/src/ConsoleScriptingInterface.cpp
index 2d9d5c7ad6..ac55ec423e 100644
--- a/libraries/script-engine/src/ConsoleScriptingInterface.cpp
+++ b/libraries/script-engine/src/ConsoleScriptingInterface.cpp
@@ -34,7 +34,7 @@ QList<QString> ConsoleScriptingInterface::_groupDetails = QList<QString>();
 
 ScriptValue ConsoleScriptingInterface::info(ScriptContext* context, ScriptEngine* engine) {
     if (ScriptManager* scriptManager = engine->manager()) {
-        scriptManager->scriptInfoMessage(appendArguments(context));
+        scriptManager->scriptInfoMessage(appendArguments(context), context->currentFileName(), context->currentLineNumber());
     }
     return engine->nullValue();
 }
@@ -43,7 +43,7 @@ ScriptValue ConsoleScriptingInterface::log(ScriptContext* context, ScriptEngine*
     QString message = appendArguments(context);
     if (_groupDetails.count() == 0) {
         if (ScriptManager* scriptManager = engine->manager()) {
-            scriptManager->scriptPrintedMessage(message);
+            scriptManager->scriptPrintedMessage(message, context->currentFileName(), context->currentLineNumber());
         }
     } else {
         logGroupMessage(message, engine);
@@ -53,28 +53,28 @@ ScriptValue ConsoleScriptingInterface::log(ScriptContext* context, ScriptEngine*
 
 ScriptValue ConsoleScriptingInterface::debug(ScriptContext* context, ScriptEngine* engine) {
     if (ScriptManager* scriptManager = engine->manager()) {
-        scriptManager->scriptPrintedMessage(appendArguments(context));
+        scriptManager->scriptPrintedMessage(appendArguments(context), context->currentFileName(), context->currentLineNumber());
     }
     return engine->nullValue();
 }
 
 ScriptValue ConsoleScriptingInterface::warn(ScriptContext* context, ScriptEngine* engine) {
     if (ScriptManager* scriptManager = engine->manager()) {
-        scriptManager->scriptWarningMessage(appendArguments(context));
+        scriptManager->scriptWarningMessage(appendArguments(context), context->currentFileName(), context->currentLineNumber());
     }
     return engine->nullValue();
 }
 
 ScriptValue ConsoleScriptingInterface::error(ScriptContext* context, ScriptEngine* engine) {
     if (ScriptManager* scriptManager = engine->manager()) {
-        scriptManager->scriptErrorMessage(appendArguments(context));
+        scriptManager->scriptErrorMessage(appendArguments(context), context->currentFileName(), context->currentLineNumber());
     }
     return engine->nullValue();
 }
 
 ScriptValue ConsoleScriptingInterface::exception(ScriptContext* context, ScriptEngine* engine) {
     if (ScriptManager* scriptManager = engine->manager()) {
-        scriptManager->scriptErrorMessage(appendArguments(context));
+        scriptManager->scriptErrorMessage(appendArguments(context), context->currentFileName(), context->currentLineNumber());
     }
     return engine->nullValue();
 }
@@ -82,23 +82,23 @@ ScriptValue ConsoleScriptingInterface::exception(ScriptContext* context, ScriptE
 void ConsoleScriptingInterface::time(QString labelName) {
     _timerDetails.insert(labelName, QDateTime::currentDateTime().toUTC());
     QString message = QString("%1: Timer started").arg(labelName);
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     if (ScriptManager* scriptManager = engine()->manager()) {
-        scriptManager->scriptPrintedMessage(message);
+        scriptManager->scriptPrintedMessage(message, context()->currentFileName(), context()->currentLineNumber());
     }
 }
 
 void ConsoleScriptingInterface::timeEnd(QString labelName) {
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     if (ScriptManager* scriptManager = engine()->manager()) {
         if (!_timerDetails.contains(labelName)) {
-            scriptManager->scriptErrorMessage("No such label found " + labelName);
+            scriptManager->scriptErrorMessage("No such label found " + labelName, context()->currentFileName(), context()->currentLineNumber());
             return;
         }
 
         if (_timerDetails.value(labelName).isNull()) {
             _timerDetails.remove(labelName);
-            scriptManager->scriptErrorMessage("Invalid start time for " + labelName);
+            scriptManager->scriptErrorMessage("Invalid start time for " + labelName, context()->currentFileName(), context()->currentLineNumber());
             return;
         }
         QDateTime _startTime = _timerDetails.value(labelName);
@@ -108,7 +108,7 @@ void ConsoleScriptingInterface::timeEnd(QString labelName) {
         QString message = QString("%1: %2ms").arg(labelName).arg(QString::number(diffInMS));
         _timerDetails.remove(labelName);
 
-        scriptManager->scriptPrintedMessage(message);
+        scriptManager->scriptPrintedMessage(message, context()->currentFileName(), context()->currentLineNumber());
     }
 }
 
@@ -131,24 +131,24 @@ ScriptValue ConsoleScriptingInterface::assertion(ScriptContext* context, ScriptE
             assertionResult = QString("Assertion failed : %1").arg(message);
         }
         if (ScriptManager* scriptManager = engine->manager()) {
-            scriptManager->scriptErrorMessage(assertionResult);
+            scriptManager->scriptErrorMessage(assertionResult, context->currentFileName(), context->currentLineNumber());
         }
     }
     return engine->nullValue();
 }
 
 void ConsoleScriptingInterface::trace() {
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     ScriptEnginePointer scriptEngine = engine();
     if (ScriptManager* scriptManager = scriptEngine->manager()) {
         scriptManager->scriptPrintedMessage
             (QString(STACK_TRACE_FORMAT).arg(LINE_SEPARATOR,
-            scriptEngine->currentContext()->backtrace().join(LINE_SEPARATOR)));
+            scriptEngine->currentContext()->backtrace().join(LINE_SEPARATOR)), context()->currentFileName(), context()->currentLineNumber());
     }
 }
 
 void ConsoleScriptingInterface::clear() {
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     if (ScriptManager* scriptManager = engine()->manager()) {
         scriptManager->clearDebugLogWindow();
     }
@@ -190,6 +190,6 @@ void ConsoleScriptingInterface::logGroupMessage(QString message, ScriptEngine* e
     }
     logMessage.append(message);
     if (ScriptManager* scriptManager = engine->manager()) {
-        scriptManager->scriptPrintedMessage(logMessage);
+        scriptManager->scriptPrintedMessage(logMessage, context()->currentFileName(), context()->currentLineNumber());
     }
 }
diff --git a/libraries/script-engine/src/HelperScriptEngine.cpp b/libraries/script-engine/src/HelperScriptEngine.cpp
new file mode 100644
index 0000000000..313fac74da
--- /dev/null
+++ b/libraries/script-engine/src/HelperScriptEngine.cpp
@@ -0,0 +1,31 @@
+//
+//  HelperScriptEngine.h
+//  libraries/script-engine/src/HelperScriptEngine.h
+//
+//  Created by dr Karol Suprynowicz on 2024/04/28.
+//  Copyright 2024 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
+//
+
+#include "HelperScriptEngine.h"
+
+HelperScriptEngine::HelperScriptEngine() {
+    std::lock_guard<std::mutex> lock(_scriptEngineLock);
+    _scriptEngine = newScriptEngine();
+    _scriptEngineThread.reset(new QThread());
+    _scriptEngine->setThread(_scriptEngineThread.get());
+    _scriptEngineThread->start();
+}
+
+HelperScriptEngine::~HelperScriptEngine() {
+    std::lock_guard<std::mutex> lock(_scriptEngineLock);
+    if (_scriptEngine) {
+        if (_scriptEngineThread) {
+            _scriptEngineThread->quit();
+            _scriptEngineThread->wait();
+        }
+        _scriptEngine.reset();
+    }
+}
diff --git a/libraries/script-engine/src/HelperScriptEngine.h b/libraries/script-engine/src/HelperScriptEngine.h
new file mode 100644
index 0000000000..53d2e89306
--- /dev/null
+++ b/libraries/script-engine/src/HelperScriptEngine.h
@@ -0,0 +1,65 @@
+//
+//  HelperScriptEngine.h
+//  libraries/script-engine/src/HelperScriptEngine.h
+//
+//  Created by dr Karol Suprynowicz on 2024/04/28.
+//  Copyright 2024 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
+//
+
+#ifndef overte_HelperScriptEngine_h
+#define overte_HelperScriptEngine_h
+
+#include <mutex>
+#include "QThread"
+
+#include "ScriptEngine.h"
+
+/**
+ * @brief Provides a wrapper around script engine that does not have ScriptManager
+ *
+ * HelperScriptEngine is used for performing smaller tasks, like for example conversions between entity
+ * properties and JSON data.
+ * For thread safety all accesses to helper script engine need to be done either through HelperScriptEngine::run()
+ * or HelperScriptEngine::runWithResult().
+ *
+ */
+
+
+class HelperScriptEngine {
+public:
+    HelperScriptEngine();
+    ~HelperScriptEngine();
+
+    template <typename F>
+    inline void run(F&& f) {
+        std::lock_guard<std::mutex> guard(_scriptEngineLock);
+        f();
+    }
+
+    template <typename T, typename F>
+    inline T runWithResult(F&& f) {
+        T result;
+        {
+            std::lock_guard<std::mutex> guard(_scriptEngineLock);
+            result = f();
+        }
+        return result;
+    }
+
+    /**
+     * @brief Returns pointer to the script engine
+     *
+     * This function should be used only inside HelperScriptEngine::run() or HelperScriptEngine::runWithResult()
+     */
+    ScriptEngine* get() { return _scriptEngine.get(); };
+    ScriptEnginePointer getShared() { return _scriptEngine; };
+private:
+    std::mutex _scriptEngineLock;
+    ScriptEnginePointer _scriptEngine { nullptr };
+    std::shared_ptr<QThread> _scriptEngineThread { nullptr };
+};
+
+#endif  //overte_HelperScriptEngine_h
diff --git a/libraries/script-engine/src/Mat4.cpp b/libraries/script-engine/src/Mat4.cpp
index fefcf6a0f6..a333d28ef8 100644
--- a/libraries/script-engine/src/Mat4.cpp
+++ b/libraries/script-engine/src/Mat4.cpp
@@ -90,7 +90,7 @@ void Mat4::print(const QString& label, const glm::mat4& m, bool transpose) const
     QString message = QString("%1 %2").arg(qPrintable(label));
     message = message.arg(glm::to_string(out).c_str());
     qCDebug(scriptengine) << message;
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     if (ScriptManager* scriptManager = engine()->manager()) {
         scriptManager->print(message);
     }
diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp
index 492534f021..3c882d7292 100644
--- a/libraries/script-engine/src/Quat.cpp
+++ b/libraries/script-engine/src/Quat.cpp
@@ -126,7 +126,7 @@ void Quat::print(const QString& label, const glm::quat& q, bool asDegrees) {
         message = message.arg(glm::to_string(glm::dquat(q)).c_str());
     }
     qCDebug(scriptengine) << message;
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     if (ScriptManager* scriptManager = engine()->manager()) {
         scriptManager->print(message);
     }
diff --git a/libraries/script-engine/src/ScriptContext.h b/libraries/script-engine/src/ScriptContext.h
index 7bc70e1080..8eb67c4247 100644
--- a/libraries/script-engine/src/ScriptContext.h
+++ b/libraries/script-engine/src/ScriptContext.h
@@ -57,6 +57,13 @@ public:
     virtual int argumentCount() const = 0;
     virtual ScriptValue argument(int index) const = 0;
     virtual QStringList backtrace() const = 0;
+
+    // Name of the file in which message was generated. Empty string when no file name is available.
+    virtual int currentLineNumber() const = 0;
+
+    // Number of the line on which message was generated. -1 if there line number is not available.
+    virtual QString currentFileName() const = 0;
+
     virtual ScriptValue callee() const = 0;
     virtual ScriptEnginePointer engine() const = 0;
     virtual ScriptFunctionContextPointer functionContext() const = 0;
diff --git a/libraries/script-engine/src/ScriptEngines.cpp b/libraries/script-engine/src/ScriptEngines.cpp
index b47255ed24..52d3b2753b 100644
--- a/libraries/script-engine/src/ScriptEngines.cpp
+++ b/libraries/script-engine/src/ScriptEngines.cpp
@@ -158,8 +158,69 @@ void ScriptEngines::removeScriptEngine(ScriptManagerPointer manager) {
         QMutexLocker locker(&_allScriptsMutex);
         _allKnownScriptManagers.remove(manager);
     }
+    std::lock_guard<std::mutex> lock(_subscriptionsToEntityScriptMessagesMutex);
+    _managersSubscribedToEntityScriptMessages.remove(manager.get());
+    _entitiesSubscribedToEntityScriptMessages.remove(manager.get());
 }
 
+void ScriptEngines::requestServerEntityScriptMessages(ScriptManager *manager) {
+    std::lock_guard<std::mutex> lock(_subscriptionsToEntityScriptMessagesMutex);
+    if (!_managersSubscribedToEntityScriptMessages.contains(manager)) {
+        _managersSubscribedToEntityScriptMessages.insert(manager);
+        // Emit a signal to inform EntityScriptServerLogClient about subscription request
+        emit requestingEntityScriptServerLog(true);
+        qDebug() << "ScriptEngines::requestServerEntityScriptMessages";
+    }
+}
+
+void ScriptEngines::requestServerEntityScriptMessages(ScriptManager *manager, const QUuid& entityID) {
+    std::lock_guard<std::mutex> lock(_subscriptionsToEntityScriptMessagesMutex);
+    if (!_entitiesSubscribedToEntityScriptMessages.contains(manager)) {
+        _entitiesSubscribedToEntityScriptMessages.insert(manager,QSet<QUuid>());
+    }
+    if (!_entitiesSubscribedToEntityScriptMessages[manager].contains(entityID)) {
+        _entitiesSubscribedToEntityScriptMessages[manager].insert(entityID);
+        // Emit a signal to inform EntityScriptServerLogClient about subscription request
+        emit requestingEntityScriptServerLog(true);
+        qDebug() << "ScriptEngines::requestServerEntityScriptMessages uuid";
+    }
+}
+
+void ScriptEngines::removeServerEntityScriptMessagesRequest(ScriptManager *manager) {
+    std::lock_guard<std::mutex> lock(_subscriptionsToEntityScriptMessagesMutex);
+    if (_managersSubscribedToEntityScriptMessages.contains(manager)) {
+        _managersSubscribedToEntityScriptMessages.remove(manager);
+    }
+    if (_entitiesSubscribedToEntityScriptMessages.isEmpty()
+        && _managersSubscribedToEntityScriptMessages.isEmpty()) {
+        // No managers requiring entity script server messages remain, so we inform EntityScriptServerLogClient about this
+        // Emit a signal to inform EntityScriptServerLogClient about subscription request
+        emit requestingEntityScriptServerLog(false);
+        qDebug() << "ScriptEngines::removeServerEntityScriptMessagesRequest";
+    }
+}
+
+void ScriptEngines::removeServerEntityScriptMessagesRequest(ScriptManager *manager, const QUuid& entityID) {
+    std::lock_guard<std::mutex> lock(_subscriptionsToEntityScriptMessagesMutex);
+    if (!_entitiesSubscribedToEntityScriptMessages.contains(manager)) {
+        return;
+    }
+    if (_entitiesSubscribedToEntityScriptMessages[manager].contains(entityID)) {
+        _entitiesSubscribedToEntityScriptMessages[manager].remove(entityID);
+    }
+    if (_entitiesSubscribedToEntityScriptMessages[manager].isEmpty()) {
+        _entitiesSubscribedToEntityScriptMessages.remove(manager);
+    }
+    if (_entitiesSubscribedToEntityScriptMessages.isEmpty()
+        && _managersSubscribedToEntityScriptMessages.isEmpty()) {
+        // No managers requiring entity script server messages remain, so we inform EntityScriptServerLogClient about this
+        // Emit a signal to inform EntityScriptServerLogClient about subscription request
+        emit requestingEntityScriptServerLog(false);
+        qDebug() << "ScriptEngines::removeServerEntityScriptMessagesRequest uuid";
+    }
+}
+
+
 void ScriptEngines::shutdownScripting() {
     _isStopped = true;
     QMutexLocker locker(&_allScriptsMutex);
diff --git a/libraries/script-engine/src/ScriptEngines.h b/libraries/script-engine/src/ScriptEngines.h
index 671789bd8e..bafaa1322c 100644
--- a/libraries/script-engine/src/ScriptEngines.h
+++ b/libraries/script-engine/src/ScriptEngines.h
@@ -186,6 +186,13 @@ public:
 
     void removeScriptEngine(ScriptManagerPointer);
 
+    // Called by ScriptManagerScriptingInterface
+    void requestServerEntityScriptMessages(ScriptManager *manager);
+    void requestServerEntityScriptMessages(ScriptManager *manager, const QUuid& entityID);
+
+    void removeServerEntityScriptMessagesRequest(ScriptManager *manager);
+    void removeServerEntityScriptMessagesRequest(ScriptManager *manager, const QUuid& entityID);
+
     ScriptGatekeeper scriptGatekeeper;
 
 signals:
@@ -251,11 +258,62 @@ signals:
      * Triggered when any script generates an information message or {@link console.info} is called.
      * @function ScriptDiscoveryService.infoMessage
      * @param {string} message - The information message.
-     * @param {string} scriptName - The name of the script that generated the informaton message.
+     * @param {string} scriptName - The name of the script that generated the information message.
      * @returns {Signal}
      */
     void infoMessage(const QString& message, const QString& engineName);
 
+    /*@jsdoc
+     * Triggered when a client side entity script prints a message to the program log via {@link  print}, {@link Script.print},
+     * {@link console.log}, {@link console.debug}, {@link console.group}, {@link console.groupEnd}, {@link console.time}, or
+     * {@link console.timeEnd}.
+     * @function Script.printedMessage
+     * @param {string} message - The message.
+     * @param {string} fileName - Name of the file in which message was generated. Empty string when no file name is available.
+     * @param {number} lineNumber - Number of the line on which message was generated. -1 if there line number is not available.
+     * @param {Uuid} entityID - Entity ID.
+     * @param {boolean} isServerScript - true if entity script is server-side, false if it is client-side.
+     * @returns {Signal}
+     */
+    void printedEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript);
+
+    /*@jsdoc
+     * Triggered when a client side entity script generates an error, {@link console.error} or {@link console.exception} is called, or
+     * {@link console.assert} is called and fails.
+     * @function Script.errorMessage
+     * @param {string} message - The error message.
+     * @param {string} fileName - Name of the file in which message was generated. Empty string when no file name is available.
+     * @param {number} lineNumber - Number of the line on which message was generated. -1 if there line number is not available.
+     * @param {Uuid} entityID - Entity ID.
+     * @param {boolean} isServerScript - true if entity script is server-side, false if it is client-side.
+     * @returns {Signal}
+     */
+    void errorEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript);
+
+    /*@jsdoc
+     * Triggered when a client side entity script generates a warning or {@link console.warn} is called.
+     * @function Script.warningMessage
+     * @param {string} message - The warning message.
+     * @param {string} fileName - Name of the file in which message was generated. Empty string when no file name is available.
+     * @param {number} lineNumber - Number of the line on which message was generated. -1 if there line number is not available.
+     * @param {Uuid} entityID - Entity ID.
+     * @param {boolean} isServerScript - true if entity script is server-side, false if it is client-side.
+     * @returns {Signal}
+     */
+    void warningEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript);
+
+    /*@jsdoc
+     * Triggered when a client side entity script generates an information message or {@link console.info} is called.
+     * @function Script.infoMessage
+     * @param {string} message - The information message.
+     * @param {string} fileName - Name of the file in which message was generated. Empty string when no file name is available.
+     * @param {number} lineNumber - Number of the line on which message was generated. -1 if there line number is not available.
+     * @param {Uuid} entityID - Entity ID.
+     * @param {boolean} isServerScript - true if entity script is server-side, false if it is client-side.
+     * @returns {Signal}
+     */
+    void infoEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript);
+
     /*@jsdoc
      * @function ScriptDiscoveryService.errorLoadingScript
      * @param {string} url - URL.
@@ -272,6 +330,12 @@ signals:
      */
     void clearDebugWindow();
 
+    /**
+     * @brief Fires when script engines need entity server script messages (areMessagesRequested == true)
+     * and when messages are not needed anymore (areMessagesRequested == false).
+     */
+    void requestingEntityScriptServerLog(bool areMessagesRequested);
+
 public slots:
 
     /*@jsdoc
@@ -355,6 +419,12 @@ protected:
     bool _defaultScriptsLocationOverridden { false };
     QString _debugScriptUrl;
 
+    // For subscriptions to server entity script messages
+    std::mutex _subscriptionsToEntityScriptMessagesMutex;
+    QSet<ScriptManager*> _managersSubscribedToEntityScriptMessages;
+    // Since multiple entity scripts run in the same script engine, there's a need to track subscriptions per entity
+    QHash<ScriptManager*, QSet<QUuid>> _entitiesSubscribedToEntityScriptMessages;
+
     // If this is set, defaultScripts.js will not be run if it is in the settings,
     // and this will be run instead. This script will not be persisted to settings.
     const QUrl _defaultScriptsOverride { };
diff --git a/libraries/script-engine/src/ScriptManager.cpp b/libraries/script-engine/src/ScriptManager.cpp
index f74bb01b71..f527fbb9e2 100644
--- a/libraries/script-engine/src/ScriptManager.cpp
+++ b/libraries/script-engine/src/ScriptManager.cpp
@@ -225,7 +225,12 @@ QString encodeEntityIdIntoEntityUrl(const QString& url, const QString& entityID)
 
 QString ScriptManager::logException(const ScriptValue& exception) {
     auto message = formatException(exception, _enableExtendedJSExceptions.get());
-    scriptErrorMessage(message);
+    auto context = _engine->currentContext();
+    if (context) {
+        scriptErrorMessage(message, context->currentFileName(), context->currentLineNumber());
+    } else {
+        scriptErrorMessage(message, "", -1);
+    }
     return message;
 }
 
@@ -330,6 +335,11 @@ ScriptManager::ScriptManager(Context context, const QString& scriptContents, con
         });
     }
 
+    //Gather entity script messages for transmission when running server side.
+    if (_type == Type::ENTITY_SERVER) {
+        ;
+    }
+
     if (!_areMetaTypesInitialized) {
         initMetaTypes();
     }
@@ -468,10 +478,10 @@ void ScriptManager::waitTillDoneRunning(bool shutdown) {
             }
         }
 #else
-        auto startedWaiting = usecTimestampNow();
+        //auto startedWaiting = usecTimestampNow();
         while (!_isDoneRunning) {
             // If the final evaluation takes too long, then tell the script engine to stop running
-            auto elapsedUsecs = usecTimestampNow() - startedWaiting;
+            //auto elapsedUsecs = usecTimestampNow() - startedWaiting;
             // TODO: This part was very unsafe and was causing crashes all the time.
             //  I disabled it for now until we find a safer solution.
             //  With it disabled now we get clean shutdowns and restarts.
@@ -514,7 +524,7 @@ void ScriptManager::waitTillDoneRunning(bool shutdown) {
         }
 #endif
 
-        scriptInfoMessage("Script Engine has stopped:" + getFilename());
+        scriptInfoMessage("Script Engine has stopped:" + getFilename(), "", -1);
     }
 }
 
@@ -532,6 +542,10 @@ QString ScriptManager::getFilename() const {
     return lastPart;
 }
 
+QString ScriptManager::getAbsoluteFilename() const {
+    return _fileNameString;
+}
+
 bool ScriptManager::hasValidScriptSuffix(const QString& scriptFileName) {
     QFileInfo fileInfo(scriptFileName);
     QString scriptSuffixToLower = fileInfo.completeSuffix().toLower();
@@ -549,7 +563,7 @@ void ScriptManager::loadURL(const QUrl& scriptURL, bool reload) {
 
     // Check that script has a supported file extension
     if (!hasValidScriptSuffix(_fileNameString)) {
-        scriptErrorMessage("File extension of file: " + _fileNameString + " is not a currently supported script type");
+        scriptErrorMessage("File extension of file: " + _fileNameString + " is not a currently supported script type", _fileNameString, -1);
         emit errorLoadingScript(_fileNameString);
         return;
     }
@@ -559,7 +573,7 @@ void ScriptManager::loadURL(const QUrl& scriptURL, bool reload) {
     scriptCache->getScriptContents(url.toString(), [this](const QString& url, const QString& scriptContents, bool isURL, bool success, const QString&status) {
         qCDebug(scriptengine) << "loadURL" << url << status << QThread::currentThread();
         if (!success) {
-            scriptErrorMessage("ERROR Loading file (" + status + "):" + url);
+            scriptErrorMessage("ERROR Loading file (" + status + "):" + url, url, -1);
             emit errorLoadingScript(_fileNameString);
             return;
         }
@@ -570,24 +584,54 @@ void ScriptManager::loadURL(const QUrl& scriptURL, bool reload) {
     }, reload, maxRetries);
 }
 
-void ScriptManager::scriptErrorMessage(const QString& message) {
+void ScriptManager::scriptErrorMessage(const QString& message, const QString& fileName, int lineNumber) {
     qCCritical(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message));
     emit errorMessage(message, getFilename());
+    if (!currentEntityIdentifier.isInvalidID()) {
+        emit errorEntityMessage(message, fileName, lineNumber, currentEntityIdentifier, isEntityServerScript());
+    } else {
+        if (isEntityServerScript()) {
+            emit errorEntityMessage(message, fileName, lineNumber, EntityItemID(), isEntityServerScript());
+        }
+    }
 }
 
-void ScriptManager::scriptWarningMessage(const QString& message) {
+void ScriptManager::scriptWarningMessage(const QString& message, const QString& fileName, int lineNumber) {
     qCWarning(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message));
     emit warningMessage(message, getFilename());
+    if (!currentEntityIdentifier.isInvalidID()) {
+        emit warningEntityMessage(message, fileName, lineNumber, currentEntityIdentifier, isEntityServerScript());
+    } else {
+        if (isEntityServerScript()) {
+            emit warningEntityMessage(message, fileName, lineNumber, EntityItemID(), isEntityServerScript());
+        }
+    }
 }
 
-void ScriptManager::scriptInfoMessage(const QString& message) {
+void ScriptManager::scriptInfoMessage(const QString& message, const QString& fileName, int lineNumber) {
     qCInfo(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message));
     emit infoMessage(message, getFilename());
+    if (!currentEntityIdentifier.isInvalidID()) {
+        emit infoEntityMessage(message, fileName, lineNumber, currentEntityIdentifier, isEntityServerScript());
+    } else {
+        if (isEntityServerScript()) {
+            emit infoEntityMessage(message, fileName, lineNumber, EntityItemID(), isEntityServerScript());
+        }
+    }
 }
 
-void ScriptManager::scriptPrintedMessage(const QString& message) {
+void ScriptManager::scriptPrintedMessage(const QString& message, const QString& fileName, int lineNumber) {
     qCDebug(scriptengine, "[%s] %s", qUtf8Printable(getFilename()), qUtf8Printable(message));
     emit printedMessage(message, getFilename());
+    if (!currentEntityIdentifier.isInvalidID()) {
+        emit printedEntityMessage(message, fileName, lineNumber, currentEntityIdentifier, isEntityServerScript());
+    } else {
+        if (isEntityServerScript()) {
+            // TODO: Some callbacks like for example websockets one right now
+            // don't set currentEntityIdentifier and there doesn't seem to be easy way to add it currently
+            emit printedEntityMessage(message, fileName, lineNumber, EntityItemID(), isEntityServerScript());
+        }
+    }
 }
 
 void ScriptManager::clearDebugLogWindow() {
@@ -912,7 +956,7 @@ void ScriptManager::run() {
         return; // bail early - avoid setting state in init(), as evaluate() will bail too
     }
 
-    scriptInfoMessage("Script Engine starting:" + getFilename());
+    scriptInfoMessage("Script Engine starting:" + getFilename(), getFilename(), -1);
 
     if (!_isInitialized) {
         init();
@@ -1064,7 +1108,7 @@ void ScriptManager::run() {
             _engine->clearExceptions();
         }
     }
-    scriptInfoMessage("Script Engine stopping:" + getFilename());
+    scriptInfoMessage("Script Engine stopping:" + getFilename(), getFilename(), -1);
 
     stopAllTimers(); // make sure all our timers are stopped if the script is ending
     emit scriptEnding();
@@ -1139,7 +1183,7 @@ void ScriptManager::updateMemoryCost(const qint64& deltaSize) {
 
 void ScriptManager::timerFired() {
     if (isStopped()) {
-        scriptWarningMessage("Script.timerFired() while shutting down is ignored... parent script:" + getFilename());
+        scriptWarningMessage("Script.timerFired() while shutting down is ignored... parent script:" + getFilename(), getFilename(), -1);
         return; // bail early
     }
 
@@ -1206,7 +1250,14 @@ QTimer* ScriptManager::setupTimerWithInterval(const ScriptValue& function, int i
 
 QTimer* ScriptManager::setInterval(const ScriptValue& function, int intervalMS) {
     if (isStopped()) {
-        scriptWarningMessage("Script.setInterval() while shutting down is ignored... parent script:" + getFilename());
+        int lineNumber = -1;
+        QString fileName = getFilename();
+        auto context = _engine->currentContext();
+        if (context) {
+            lineNumber = context->currentLineNumber();
+            fileName = context->currentFileName();
+        }
+        scriptWarningMessage("Script.setInterval() while shutting down is ignored... parent script:" + getFilename(), fileName, lineNumber);
         return NULL; // bail early
     }
 
@@ -1215,7 +1266,14 @@ QTimer* ScriptManager::setInterval(const ScriptValue& function, int intervalMS)
 
 QTimer* ScriptManager::setTimeout(const ScriptValue& function, int timeoutMS) {
     if (isStopped()) {
-        scriptWarningMessage("Script.setTimeout() while shutting down is ignored... parent script:" + getFilename());
+        int lineNumber = -1;
+        QString fileName = getFilename();
+        auto context = _engine->currentContext();
+        if (context) {
+            lineNumber = context->currentLineNumber();
+            fileName = context->currentFileName();
+        }
+        scriptWarningMessage("Script.setTimeout() while shutting down is ignored... parent script:" + getFilename(), fileName, lineNumber);
         return NULL; // bail early
     }
 
@@ -1281,7 +1339,7 @@ QUrl ScriptManager::resourcesPath() const {
 }
 
 void ScriptManager::print(const QString& message) {
-    emit printedMessage(message, getFilename());
+    emit scriptPrintedMessage(message, getFilename(), engine()->currentContext()->currentLineNumber());
 }
 
 
@@ -1651,8 +1709,15 @@ void ScriptManager::include(const QStringList& includeFiles, const ScriptValue&
         return;
     }
     if (isStopped()) {
+        int lineNumber = -1;
+        QString fileName = getFilename();
+        auto context = _engine->currentContext();
+        if (context) {
+            lineNumber = context->currentLineNumber();
+            fileName = context->currentFileName();
+        }
         scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:"
-                + includeFiles.join(",") + "parent script:" + getFilename());
+                + includeFiles.join(",") + "parent script:" + getFilename(), fileName, lineNumber);
         return; // bail early
     }
     QList<QUrl> urls;
@@ -1665,8 +1730,15 @@ void ScriptManager::include(const QStringList& includeFiles, const ScriptValue&
             thisURL = expandScriptUrl(QUrl::fromLocalFile(expandScriptPath(file)));
             QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation();
             if (!defaultScriptsLoc.isParentOf(thisURL)) {
+                int lineNumber = -1;
+                QString fileName = getFilename();
+                auto context = _engine->currentContext();
+                if (context) {
+                    lineNumber = context->currentLineNumber();
+                    fileName = context->currentFileName();
+                }
                 //V8TODO this probably needs to be done per context, otherwise file cannot be included again in a module
-                scriptWarningMessage("Script.include() -- skipping" + file + "-- outside of standard libraries");
+                scriptWarningMessage("Script.include() -- skipping" + file + "-- outside of standard libraries", fileName, lineNumber);
                 continue;
             }
             isStandardLibrary = true;
@@ -1676,8 +1748,15 @@ void ScriptManager::include(const QStringList& includeFiles, const ScriptValue&
 
         bool disallowOutsideFiles = thisURL.isLocalFile() && !isStandardLibrary && !currentSandboxURL.isLocalFile();
         if (disallowOutsideFiles && !PathUtils::isDescendantOf(thisURL, currentSandboxURL)) {
+            int lineNumber = -1;
+            QString fileName = currentSandboxURL.toString();
+            auto context = _engine->currentContext();
+            if (context) {
+                lineNumber = context->currentLineNumber();
+                fileName = context->currentFileName();
+            }
             scriptWarningMessage("Script.include() ignoring file path" + thisURL.toString()
-                                + "outside of original entity script" + currentSandboxURL.toString());
+                                + "outside of original entity script" + currentSandboxURL.toString(), fileName, lineNumber);
         } else {
             // We could also check here for CORS, but we don't yet.
             // It turns out that QUrl.resolve will not change hosts and copy authority, so we don't need to check that here.
@@ -1699,7 +1778,14 @@ void ScriptManager::include(const QStringList& includeFiles, const ScriptValue&
         for (QUrl url : urls) {
             QString contents = data[url];
             if (contents.isNull()) {
-                scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString());
+                int lineNumber = -1;
+                QString fileName = url.toString();
+                auto context = _engine->currentContext();
+                if (context) {
+                    lineNumber = context->currentLineNumber();
+                    fileName = context->currentFileName();
+                }
+                scriptErrorMessage("Error loading file (" + status[url] +"): " + url.toString(), fileName, lineNumber);
             } else {
                 std::lock_guard<std::recursive_mutex> lock(_lock);
                 if (!_includedURLs.contains(url)) {
@@ -1719,7 +1805,14 @@ void ScriptManager::include(const QStringList& includeFiles, const ScriptValue&
                         _engine->clearExceptions();
                     }
                 } else {
-                    scriptPrintedMessage("Script.include() skipping evaluation of previously included url:" + url.toString());
+                    int lineNumber = -1;
+                    QString fileName = url.toString();
+                    auto context = _engine->currentContext();
+                    if (context) {
+                        lineNumber = context->currentLineNumber();
+                        fileName = context->currentFileName();
+                    }
+                    scriptPrintedMessage("Script.include() skipping evaluation of previously included url:" + url.toString(), fileName, lineNumber);
                 }
             }
         }
@@ -1748,8 +1841,15 @@ void ScriptManager::include(const QStringList& includeFiles, const ScriptValue&
 
 void ScriptManager::include(const QString& includeFile, const ScriptValue& callback) {
     if (isStopped()) {
+        int lineNumber = -1;
+        QString fileName = currentSandboxURL.toString();
+        auto context = _engine->currentContext();
+        if (context) {
+            lineNumber = context->currentLineNumber();
+            fileName = context->currentFileName();
+        }
         scriptWarningMessage("Script.include() while shutting down is ignored...  includeFile:"
-                    + includeFile + "parent script:" + getFilename());
+                    + includeFile + "parent script:" + getFilename(), fileName, lineNumber);
         return; // bail early
     }
 
@@ -1765,14 +1865,21 @@ void ScriptManager::load(const QString& loadFile) {
     if (!_engine->IS_THREADSAFE_INVOCATION(__FUNCTION__)) {
         return;
     }
+    int lineNumber = -1;
+    QString fileName = getFilename();
+    auto context = _engine->currentContext();
+    if (context) {
+        lineNumber = context->currentLineNumber();
+        fileName = context->currentFileName();
+    }
     if (isStopped()) {
         scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:"
-                + loadFile + "parent script:" + getFilename());
+                + loadFile + "parent script:" + getFilename(), fileName, lineNumber);
         return; // bail early
     }
     if (!currentEntityIdentifier.isInvalidID()) {
         scriptWarningMessage("Script.load() from entity script is ignored...  loadFile:"
-                + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString());
+                + loadFile + "parent script:" + getFilename() + "entity: " + currentEntityIdentifier.toString(), fileName, lineNumber);
         return; // bail early
     }
 
@@ -2440,7 +2547,7 @@ void ScriptManager::refreshFileScript(const EntityItemID& entityID) {
         QString filePath = QUrl(details.scriptText).toLocalFile();
         auto lastModified = QFileInfo(filePath).lastModified().toMSecsSinceEpoch();
         if (lastModified > details.lastModified) {
-            scriptInfoMessage("Reloading modified script " + details.scriptText);
+            scriptInfoMessage("Reloading modified script " + details.scriptText, filePath, -1);
             loadEntityScript(entityID, details.scriptText, true);
         }
     }
diff --git a/libraries/script-engine/src/ScriptManager.h b/libraries/script-engine/src/ScriptManager.h
index 01d0a1dbf0..8197f26285 100644
--- a/libraries/script-engine/src/ScriptManager.h
+++ b/libraries/script-engine/src/ScriptManager.h
@@ -430,6 +430,13 @@ public:
      */
     QString getFilename() const;
 
+    /**
+     * @brief Get the filename of the running script, with absolute path.
+     *
+     * @return QString Filename
+     */
+    QString getAbsoluteFilename() const;
+
     /**
      * @brief Underlying scripting engine
      *
@@ -1074,8 +1081,10 @@ public:
      * Emits errorMessage()
      *
      * @param message Message to send to the log
+     * @param fileName Name of the file in which message was generated. Empty string when no file name is available.
+     * @param lineNumber Number of the line on which message was generated. -1 if there line number is not available.
      */
-    void scriptErrorMessage(const QString& message);
+    void scriptErrorMessage(const QString& message, const QString& fileName, int lineNumber);
 
     /**
      * @brief Logs a script warning message and emits an warningMessage event
@@ -1083,8 +1092,10 @@ public:
      * Emits warningMessage()
      *
      * @param message Message to send to the log
+     * @param fileName Name of the file in which message was generated. Empty string when no file name is available.
+     * @param lineNumber Number of the line on which message was generated. -1 if there line number is not available.
      */
-    void scriptWarningMessage(const QString& message);
+    void scriptWarningMessage(const QString& message, const QString& fileName, int lineNumber);
 
     /**
      * @brief Logs a script info message and emits an infoMessage event
@@ -1092,8 +1103,10 @@ public:
      * Emits infoMessage()
      *
      * @param message Message to send to the log
+     * @param fileName Name of the file in which message was generated. Empty string when no file name is available.
+     * @param lineNumber Number of the line on which message was generated. -1 if there line number is not available.
      */
-    void scriptInfoMessage(const QString& message);
+    void scriptInfoMessage(const QString& message, const QString& fileName, int lineNumber);
 
     /**
      * @brief Logs a script printed message and emits an printedMessage event
@@ -1102,9 +1115,11 @@ public:
      * Emits printedMessage()
      *
      * @param message Message to send to the log
+     * @param fileName Name of the file in which message was generated. Empty string when no file name is available.
+     * @param lineNumber Number of the line on which message was generated. -1 if there line number is not available.
      */
 
-    void scriptPrintedMessage(const QString& message);
+    void scriptPrintedMessage(const QString& message, const QString& fileName, int lineNumber);
 
     /**
      * @brief Clears the debug log window
@@ -1321,6 +1336,54 @@ signals:
      */
     void infoMessage(const QString& message, const QString& scriptName);
 
+    /**
+     * @brief Triggered when a client side entity script prints a message to the program log
+     *
+     * @param message
+     * @param fileName Name of the file in which message was generated.
+     * @param lineNumber Number of the line on which message was generated.
+     * @param entityID
+     * @param isServerScript true if entity script is server-side, false if it is client-side.
+     */
+    void printedEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript);
+
+
+    /**
+     * @brief Triggered when a client side entity script generates an error
+     *
+     * @param message
+     * @param fileName Name of the file in which message was generated.
+     * @param lineNumber Number of the line on which message was generated.
+     * @param entityID
+     * @param isServerScript true if entity script is server-side, false if it is client-side.
+     */
+    void errorEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript);
+
+
+
+    /**
+     * @brief  Triggered when a client side entity script generates a warning
+     *
+     * @param message
+     * @param fileName Name of the file in which message was generated.
+     * @param lineNumber Number of the line on which message was generated.
+     * @param entityID
+     * @param isServerScript true if entity script is server-side, false if it is client-side.
+     */
+    void warningEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript);
+
+
+    /**
+     * @brief Triggered when a client side entity script generates an information message
+     *
+     * @param message
+     * @param fileName Name of the file in which message was generated.
+     * @param lineNumber Number of the line on which message was generated.
+     * @param entityID
+     * @param isServerScript true if entity script is server-side, false if it is client-side.
+     */
+    void infoEntityMessage(const QString& message, const QString& fileName, int lineNumber, const EntityItemID& entityID, bool isServerScript);
+
 
     /**
      * @brief Triggered when the running state of the script changes, e.g., from running to stopping.
diff --git a/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp b/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp
index 7c9f264327..36da99b751 100644
--- a/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp
+++ b/libraries/script-engine/src/ScriptManagerScriptingInterface.cpp
@@ -12,6 +12,7 @@
 
 #include "ScriptManager.h"
 #include "ScriptManagerScriptingInterface.h"
+#include "ScriptEngines.h"
 #include "ScriptEngine.h"
 #include <QMetaType>
 
@@ -88,3 +89,47 @@ void ScriptManagerScriptingInterface::startProfiling() {
 void ScriptManagerScriptingInterface::stopProfilingAndSave() {
     _manager->engine()->stopProfilingAndSave();
 }
+
+void ScriptManagerScriptingInterface::requestServerEntityScriptMessages() {
+    if (_manager->isEntityServerScript() || _manager->isEntityServerScript()) {
+        _manager->engine()->raiseException("Uuid needs to be specified when requestServerEntityScriptMessages is invoked from entity script");
+    } else {
+        auto scriptEngines = DependencyManager::get<ScriptEngines>().data();
+        scriptEngines->requestServerEntityScriptMessages(_manager);
+    }
+}
+
+void ScriptManagerScriptingInterface::requestServerEntityScriptMessages(const QUuid& entityID) {
+    if (_manager->isEntityServerScript() || _manager->isEntityServerScript()) {
+        auto scriptEngines = DependencyManager::get<ScriptEngines>().data();
+        scriptEngines->requestServerEntityScriptMessages(_manager, entityID);
+    } else {
+        _manager->engine()->raiseException("Uuid must not be specified when requestServerEntityScriptMessages is invoked from entity script");
+    }
+}
+
+void ScriptManagerScriptingInterface::removeServerEntityScriptMessagesRequest() {
+    if (_manager->isEntityServerScript() || _manager->isEntityServerScript()) {
+        _manager->engine()->raiseException("Uuid needs to be specified when removeServerEntityScriptMessagesRequest is invoked from entity script");
+    } else {
+        auto scriptEngines = DependencyManager::get<ScriptEngines>().data();
+        scriptEngines->removeServerEntityScriptMessagesRequest(_manager);
+    }
+}
+
+void ScriptManagerScriptingInterface::removeServerEntityScriptMessagesRequest(const QUuid& entityID) {
+    if (_manager->isEntityServerScript() || _manager->isEntityServerScript()) {
+        auto scriptEngines = DependencyManager::get<ScriptEngines>().data();
+        scriptEngines->removeServerEntityScriptMessagesRequest(_manager, entityID);
+    } else {
+        _manager->engine()->raiseException("Uuid must not be specified when removeServerEntityScriptMessagesRequest is invoked from entity script");
+    }
+}
+
+QString ScriptManagerScriptingInterface::btoa(const QByteArray &binary) {
+    return binary.toBase64();
+}
+
+QByteArray ScriptManagerScriptingInterface::atob(const QString &base64) {
+    return QByteArray::fromBase64(base64.toUtf8());
+}
diff --git a/libraries/script-engine/src/ScriptManagerScriptingInterface.h b/libraries/script-engine/src/ScriptManagerScriptingInterface.h
index 119cbadaa6..d319cd30ae 100644
--- a/libraries/script-engine/src/ScriptManagerScriptingInterface.h
+++ b/libraries/script-engine/src/ScriptManagerScriptingInterface.h
@@ -512,7 +512,7 @@ public:
 
     /*@jsdoc
      * Start collecting object statistics that can later be reported with Script.dumpHeapObjectStatistics().
-     * @function Script.dumpHeapObjectStatistics
+     * @function Script.startCollectingObjectStatistics
      */
     Q_INVOKABLE void startCollectingObjectStatistics();
 
@@ -557,7 +557,47 @@ public:
      */
      Q_INVOKABLE void stopProfilingAndSave();
 
-signals:
+     /*@jsdoc
+     * After calling this function current script engine will start receiving server-side entity script messages
+     * through signals such as errorEntityMessage. This function can be invoked both from client-side entity scripts
+     * and from interface scripts.
+     * @function Script.subscribeToServerEntityScriptMessages
+     * @param {Uuid=} entityID - The ID of the entity that requests entity server script messages. Only needs to be specified
+     * for entity scripts, and must not be specified for other types of scripts.
+     */
+
+     Q_INVOKABLE void requestServerEntityScriptMessages();
+     Q_INVOKABLE void requestServerEntityScriptMessages(const QUuid& entityID);
+
+     /*@jsdoc
+     * Calling this function signalizes that current script doesn't require stop receiving server-side entity script messages
+     * through signals such as errorEntityMessage. This function can be invoked both from client-side entity scripts
+     * and from interface scripts.
+     * @function Script.unsubscribeFromServerEntityScriptMessages
+     * @param {Uuid=} entityID - The ID of the entity that requests entity server script messages. Only needs to be specified
+     * for entity scripts, and must not be specified for other types of scripts.
+     */
+
+     Q_INVOKABLE void removeServerEntityScriptMessagesRequest();
+     Q_INVOKABLE void removeServerEntityScriptMessagesRequest(const QUuid& entityID);
+
+     /*@jsdoc
+     * This decodes Base64 string and returns contents as ArrayBuffer.
+     * @function Script.atob
+     * @param {String} base64 - String with Base64-encoded binary data.
+     * @returns {ArrayBuffer} Decoded binary data.
+     */
+     Q_INVOKABLE QByteArray atob(const QString &base64);
+
+     /*@jsdoc
+     * This encodes ArrayBuffer and returns Base64-encoded string.
+     * @function Script.btoa
+     * @param {ArrayBuffer} binary - Data to be encoded.
+     * @returns {String} String with Base64-encoded binary data.
+     */
+     Q_INVOKABLE QString btoa(const QByteArray &binary);
+
+ signals:
 
     /*@jsdoc
      * @function Script.scriptLoaded
diff --git a/libraries/script-engine/src/ScriptMessage.cpp b/libraries/script-engine/src/ScriptMessage.cpp
new file mode 100644
index 0000000000..0b3ea1abc8
--- /dev/null
+++ b/libraries/script-engine/src/ScriptMessage.cpp
@@ -0,0 +1,48 @@
+//
+//  ScriptMessage.h
+//  libraries/script-engine/src/v8/FastScriptValueUtils.cpp
+//
+//  Created by dr Karol Suprynowicz on 2023/09/24.
+//  Copyright 2023 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
+//
+
+#include "ScriptMessage.h"
+
+#include <qhash.h>
+
+QJsonObject ScriptMessage::toJson() {
+    QJsonObject object;
+    object["message"] = _messageContent;
+    object["lineNumber"] = _lineNumber;
+    object["fileName"] = _fileName;
+    object["entityID"] = _entityID.toString();
+    object["type"] = static_cast<int>(_scriptType);
+    object["severity"] = static_cast<int>(_severity);
+    return object;
+}
+
+bool ScriptMessage::fromJson(const QJsonObject &object) {
+    if (object.isEmpty()) {
+        qDebug() << "ScriptMessage::fromJson object is empty";
+        return false;
+    }
+    if (!object["message"].isString()
+        || !object["lineNumber"].isDouble()
+        || !object["fileName"].isString()
+        || !object["entityID"].isString()
+        || !object["type"].isDouble()
+        || !object["severity"].isDouble()) {
+        qDebug() << "ScriptMessage::fromJson failed to find required fields in JSON file";
+        return false;
+    }
+    _messageContent = object["message"].toString();
+    _lineNumber = object["lineNumber"].toInt();
+    _fileName = object["fileName"].toInt();
+    _entityID = QUuid::fromString(object["entityID"].toString());
+    _scriptType = static_cast<ScriptMessage::ScriptType>(object["type"].toInt());
+    _severity = static_cast<ScriptMessage::Severity>(object["severity"].toInt());
+    return true;
+}
\ No newline at end of file
diff --git a/libraries/script-engine/src/ScriptMessage.h b/libraries/script-engine/src/ScriptMessage.h
new file mode 100644
index 0000000000..fb9ae25f40
--- /dev/null
+++ b/libraries/script-engine/src/ScriptMessage.h
@@ -0,0 +1,64 @@
+//
+//  ScriptMessage.h
+//  libraries/script-engine/src/v8/FastScriptValueUtils.cpp
+//
+//  Created by dr Karol Suprynowicz on 2023/09/24.
+//  Copyright 2023 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
+//
+
+#ifndef OVERTE_SCRIPTMESSAGE_H
+#define OVERTE_SCRIPTMESSAGE_H
+
+// Used to store script messages on entity script server before transmitting them to clients who subscribed to them.
+// EntityServerScriptLog packet type is used.
+// In the future will also be used for storing assignment client script messages before transmission
+
+#include <QString>
+#include <QJsonObject>
+#include "EntityItemID.h"
+
+// SEVERITY_ERROR is defined as a macro in winerror.h
+#undef SEVERITY_ERROR
+
+class ScriptMessage {
+public:
+    enum class ScriptType {
+        TYPE_NONE,
+        TYPE_ENTITY_SCRIPT
+    };
+    enum class Severity {
+        SEVERITY_NONE,
+        SEVERITY_PRINT,
+        SEVERITY_INFO,
+        SEVERITY_DEBUG,
+        SEVERITY_WARNING,
+        SEVERITY_ERROR
+    };
+
+    ScriptMessage() {};
+    ScriptMessage(const QString &messageContent, const QString &fileName, int lineNumber, const EntityItemID& entityID, ScriptType scriptType, Severity severity)
+        : _messageContent(messageContent), _fileName(fileName), _lineNumber(lineNumber), _entityID(entityID), _scriptType(scriptType), _severity(severity) {}
+
+    QJsonObject toJson();
+    bool fromJson(const QJsonObject &object);
+
+    QString getMessage() { return _messageContent; }
+    QString getFileName() { return _fileName; }
+    int getLineNumber() { return _lineNumber; }
+    ScriptType getScriptType() { return _scriptType; }
+    Severity getSeverity() { return _severity; }
+    EntityItemID getEntityID() { return _entityID; }
+
+private:
+    QString _messageContent;
+    QString _fileName;
+    int _lineNumber {-1};
+    EntityItemID _entityID;
+    ScriptType _scriptType {ScriptType::TYPE_NONE};
+    Severity _severity {Severity::SEVERITY_NONE};
+};
+
+#endif  //OVERTE_SCRIPTMESSAGE_H
diff --git a/libraries/script-engine/src/ScriptPermissions.cpp b/libraries/script-engine/src/ScriptPermissions.cpp
new file mode 100644
index 0000000000..a817f00056
--- /dev/null
+++ b/libraries/script-engine/src/ScriptPermissions.cpp
@@ -0,0 +1,124 @@
+//
+//  ScriptPermissions.cpp
+//  libraries/script-engine/src/ScriptPermissions.cpp
+//
+//  Created by dr Karol Suprynowicz on 2024/03/24.
+//  Copyright 2024 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
+//
+
+#include "ScriptPermissions.h"
+
+#include <array>
+#include <QJsonArray>
+
+#include "ScriptEngine.h"
+#include "ScriptManager.h"
+#include "Scriptable.h"
+
+static const bool PERMISSIONS_DEBUG_ENABLED = false;
+
+extern const std::array<QString, static_cast<int>(ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE)> scriptPermissionNames {
+    "Permission to get user's avatar URL" //SCRIPT_PERMISSION_GET_AVATAR_URL
+};
+
+extern const std::array<QString, static_cast<int>(ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE)> scriptPermissionSettingKeyNames {
+    "private/scriptPermissionGetAvatarURLSafeURLs" //SCRIPT_PERMISSION_GET_AVATAR_URL
+};
+
+extern const std::array<QString, static_cast<int>(ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE)> scriptPermissionSettingEnableKeyNames {
+    "private/scriptPermissionGetAvatarURLEnable" //SCRIPT_PERMISSION_GET_AVATAR_URL
+};
+
+extern const std::array<bool, static_cast<int>(ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE)> scriptPermissionSettingEnableDefaultValues {
+    true //SCRIPT_PERMISSION_GET_AVATAR_URL
+};
+
+bool ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission permission) {
+    if (permission >= ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE) {
+        return false;
+    }
+    int permissionIndex = static_cast<int>(permission);
+    // Check if the permission checking is active
+    Setting::Handle<bool> isCheckingEnabled(scriptPermissionSettingEnableKeyNames[permissionIndex], scriptPermissionSettingEnableDefaultValues[permissionIndex]);
+    if (!isCheckingEnabled.get()) {
+        return true;
+    }
+    // Get the script manager:
+    auto engine = Scriptable::engine();
+    if (!engine) {
+        // When this happens it means that function was called from QML or C++ and should always be allowed
+        if (PERMISSIONS_DEBUG_ENABLED) {
+            qDebug() << "ScriptPermissions::isCurrentScriptAllowed called outside script engine for permission: "
+                     << scriptPermissionNames[permissionIndex];
+        }
+        return true;
+    }
+    auto manager = engine->manager();
+    if (!manager) {
+        qDebug() << "ScriptPermissions::isCurrentScriptAllowed called from script engine with no script manager for permission: " << scriptPermissionNames[permissionIndex];
+        return false;
+    }
+    std::vector<QString> urlsToCheck;
+    QString scriptURL = manager->getAbsoluteFilename();
+
+    // If this is an entity script manager, we need to find the file name of the current script instead
+    if (!scriptURL.startsWith("about:Entities")) {
+        urlsToCheck.push_back(scriptURL);
+    }
+
+    auto currentURL = Scriptable::context()->currentFileName();
+    if (!currentURL.isEmpty() && currentURL != scriptURL) {
+        urlsToCheck.push_back(currentURL);
+    }
+
+    if (PERMISSIONS_DEBUG_ENABLED) {
+        qDebug() << "ScriptPermissions::isCurrentScriptAllowed: filename: " << scriptURL;
+    }
+    auto parentContext = Scriptable::context()->parentContext();
+    while (parentContext) {
+        QString parentFilename = parentContext->currentFileName();
+        if (!parentFilename.isEmpty()) {
+            urlsToCheck.push_back(parentContext->currentFileName());
+            if (PERMISSIONS_DEBUG_ENABLED) {
+                qDebug() << "ScriptPermissions::isCurrentScriptAllowed: parent filename: " << parentContext->currentFileName();
+            }
+        }
+        parentContext = parentContext->parentContext();
+    }
+
+    // Check if the script is allowed:
+    QList<QString> safeURLPrefixes = { "file:///", "qrc:/", NetworkingConstants::OVERTE_COMMUNITY_APPLICATIONS,
+                                       NetworkingConstants::OVERTE_TUTORIAL_SCRIPTS, "about:console"};
+    Setting::Handle<QString> allowedURLsSetting(scriptPermissionSettingKeyNames[permissionIndex]);
+    QList<QString> allowedURLs = allowedURLsSetting.get().split("\n");
+
+    for (auto entry : allowedURLs) {
+        safeURLPrefixes.push_back(entry);
+    }
+
+    for (auto urlToCheck : urlsToCheck) {
+        bool urlIsAllowed = false;
+        for (const auto& str : safeURLPrefixes) {
+            if (!str.isEmpty() && urlToCheck.startsWith(str)) {
+                urlIsAllowed = true;
+                if (PERMISSIONS_DEBUG_ENABLED) {
+                    qDebug() << "ScriptPermissions::isCurrentScriptAllowed: " << scriptPermissionNames[permissionIndex]
+                             << " for script " << urlToCheck << " accepted with rule: " << str;
+                }
+            }
+        }
+
+        if (!urlIsAllowed) {
+            if (PERMISSIONS_DEBUG_ENABLED) {
+                qDebug() << "ScriptPermissions::isCurrentScriptAllowed: " << scriptPermissionNames[permissionIndex]
+                         << " for script " << urlToCheck << " rejected.";
+            }
+            return false;
+        }
+    }
+
+    return true;
+}
\ No newline at end of file
diff --git a/libraries/script-engine/src/ScriptPermissions.h b/libraries/script-engine/src/ScriptPermissions.h
new file mode 100644
index 0000000000..f4b06253c5
--- /dev/null
+++ b/libraries/script-engine/src/ScriptPermissions.h
@@ -0,0 +1,31 @@
+//
+//  ScriptPermissions.h
+//  libraries/script-engine/src/ScriptPermissions.h
+//
+//  Created by dr Karol Suprynowicz on 2024/03/24.
+//  Copyright 2024 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
+//
+
+#pragma once
+
+#include <vector>
+
+#include "SettingHandle.h"
+#include "DependencyManager.h"
+
+class ScriptPermissions {
+public:
+    enum class Permission {
+        SCRIPT_PERMISSION_GET_AVATAR_URL,
+        SCRIPT_PERMISSIONS_SIZE
+    };
+
+    static bool isCurrentScriptAllowed(Permission permission);
+    //TODO: add a function to request permission through a popup
+};
+
+// TODO: add ScriptPermissionsScriptingInterface, where script can check if they have permissions
+// and request permissions through a tablet popup.
diff --git a/libraries/script-engine/src/ScriptUUID.cpp b/libraries/script-engine/src/ScriptUUID.cpp
index a6c054d2f6..376796c0dd 100644
--- a/libraries/script-engine/src/ScriptUUID.cpp
+++ b/libraries/script-engine/src/ScriptUUID.cpp
@@ -45,7 +45,7 @@ void ScriptUUID::print(const QString& label, const QUuid& id) {
     QString message = QString("%1 %2").arg(qPrintable(label));
     message = message.arg(id.toString());
     qCDebug(scriptengine) << message;
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     if (ScriptManager* scriptManager = engine()->manager()) {
         scriptManager->print(message);
     }
diff --git a/libraries/script-engine/src/ScriptValueUtils.cpp b/libraries/script-engine/src/ScriptValueUtils.cpp
index f5808080ba..77ee38de8c 100644
--- a/libraries/script-engine/src/ScriptValueUtils.cpp
+++ b/libraries/script-engine/src/ScriptValueUtils.cpp
@@ -79,6 +79,7 @@ void registerMetaTypes(ScriptEngine* engine) {
     scriptRegisterMetaType<QUrl, qURLToScriptValue, qURLFromScriptValue>(engine);
     scriptRegisterMetaType<QColor, qColorToScriptValue, qColorFromScriptValue>(engine);
     scriptRegisterMetaType<QTimer*, qTimerToScriptValue, qTimerFromScriptValue>(engine, "QTimer*");
+    scriptRegisterMetaType<QByteArray, qBytearrayToScriptValue, qBytearrayFromScriptValue>(engine, "QByteArray");
 
     scriptRegisterMetaType<PickRay, pickRayToScriptValue, pickRayFromScriptValue>(engine);
     scriptRegisterMetaType<Collision, collisionToScriptValue, collisionFromScriptValue>(engine);
diff --git a/libraries/script-engine/src/Vec3.cpp b/libraries/script-engine/src/Vec3.cpp
index 6b689d4c4c..95897dc276 100644
--- a/libraries/script-engine/src/Vec3.cpp
+++ b/libraries/script-engine/src/Vec3.cpp
@@ -39,7 +39,7 @@ void Vec3::print(const QString& label, const glm::vec3& v) {
     QString message = QString("%1 %2").arg(qPrintable(label));
     message = message.arg(glm::to_string(glm::dvec3(v)).c_str());
     qCDebug(scriptengine) << message;
-    Q_ASSERT(engine);
+    Q_ASSERT(engine());
     if (ScriptManager* scriptManager = engine()->manager()) {
         scriptManager->print(message);
     }
diff --git a/libraries/script-engine/src/v8/FastScriptValueUtils.cpp b/libraries/script-engine/src/v8/FastScriptValueUtils.cpp
index 66c5fce2e4..d45b01b303 100644
--- a/libraries/script-engine/src/v8/FastScriptValueUtils.cpp
+++ b/libraries/script-engine/src/v8/FastScriptValueUtils.cpp
@@ -19,6 +19,48 @@
 
 #ifdef CONVERSIONS_OPTIMIZED_FOR_V8
 
+ScriptValue qBytearrayToScriptValue(ScriptEngine* engine, const QByteArray &qByteArray) {
+    auto engineV8 = dynamic_cast<ScriptEngineV8*>(engine);
+    Q_ASSERT(engineV8);
+    auto isolate = engineV8->getIsolate();
+    v8::Locker locker(isolate);
+    v8::Isolate::Scope isolateScope(isolate);
+    v8::HandleScope handleScope(isolate);
+    auto context = engineV8->getContext();
+    v8::Context::Scope contextScope(context);
+    v8::Local<v8::ArrayBuffer> arrayBuffer = v8::ArrayBuffer::New(isolate, qByteArray.size());
+    memcpy(arrayBuffer->GetBackingStore()->Data(), qByteArray.data(), qByteArray.size());
+    v8::Local<v8::Value> arrayBufferValue = v8::Local<v8::Value>::Cast(arrayBuffer);
+
+    return {new ScriptValueV8Wrapper(engineV8, V8ScriptValue(engineV8, arrayBufferValue))};
+}
+
+bool qBytearrayFromScriptValue(const ScriptValue& object, QByteArray &qByteArray) {
+    ScriptValueV8Wrapper *proxy = ScriptValueV8Wrapper::unwrap(object);
+    if (!proxy) {
+        return false;
+    }
+
+    auto engineV8 = proxy->getV8Engine();
+
+    auto isolate = engineV8->getIsolate();
+    v8::Locker locker(isolate);
+    v8::Isolate::Scope isolateScope(isolate);
+    v8::HandleScope handleScope(isolate);
+    auto context = engineV8->getContext();
+    v8::Context::Scope contextScope(context);
+    V8ScriptValue v8ScriptValue = proxy->toV8Value();
+
+    v8::Local<v8::Value> v8Value = v8ScriptValue.get();
+    if(!v8Value->IsArrayBuffer()) {
+        return false;
+    }
+    v8::Local<v8::ArrayBuffer> arrayBuffer = v8::Local<v8::ArrayBuffer>::Cast(v8Value);
+    qByteArray.resize((int)arrayBuffer->ByteLength());
+    memcpy(qByteArray.data(), arrayBuffer->Data(), arrayBuffer->ByteLength());
+    return true;
+}
+
 ScriptValue vec3ToScriptValue(ScriptEngine* engine, const glm::vec3& vec3) {
     ScriptValue value = engine->newObject();
 
@@ -31,7 +73,7 @@ ScriptValue vec3ToScriptValue(ScriptEngine* engine, const glm::vec3& vec3) {
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
     auto context = engineV8->getContext();
-    v8::Context::Scope contextScope(engineV8->getContext());
+    v8::Context::Scope contextScope(context);
     V8ScriptValue v8ScriptValue = proxy->toV8Value();
     v8::Local<v8::Object> v8Object = v8::Local<v8::Object>::Cast(v8ScriptValue.get());
 
@@ -98,6 +140,9 @@ ScriptValue vec3ToScriptValue(ScriptEngine* engine, const glm::vec3& vec3) {
 
 bool vec3FromScriptValue(const ScriptValue& object, glm::vec3& vec3) {
     ScriptValueV8Wrapper *proxy = ScriptValueV8Wrapper::unwrap(object);
+    if (!proxy) {
+        return false;
+    }
 
     auto engineV8 = proxy->getV8Engine();
 
@@ -106,7 +151,7 @@ bool vec3FromScriptValue(const ScriptValue& object, glm::vec3& vec3) {
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
     auto context = engineV8->getContext();
-    v8::Context::Scope contextScope(engineV8->getContext());
+    v8::Context::Scope contextScope(context);
     V8ScriptValue v8ScriptValue = proxy->toV8Value();
 
     v8::Local<v8::Value> v8Value = v8ScriptValue.get();
diff --git a/libraries/script-engine/src/v8/FastScriptValueUtils.h b/libraries/script-engine/src/v8/FastScriptValueUtils.h
index 80d97023a5..00deb04d9c 100644
--- a/libraries/script-engine/src/v8/FastScriptValueUtils.h
+++ b/libraries/script-engine/src/v8/FastScriptValueUtils.h
@@ -24,6 +24,10 @@
 #define CONVERSIONS_OPTIMIZED_FOR_V8
 
 #ifdef CONVERSIONS_OPTIMIZED_FOR_V8
+ScriptValue qBytearrayToScriptValue(ScriptEngine* engine, const QByteArray &qByteArray);
+
+bool qBytearrayFromScriptValue(const ScriptValue& object, QByteArray &qByteArray);
+
 ScriptValue vec3ToScriptValue(ScriptEngine* engine, const glm::vec3& vec3);
 
 bool vec3FromScriptValue(const ScriptValue& object, glm::vec3& vec3);
diff --git a/libraries/script-engine/src/v8/ScriptContextV8Wrapper.cpp b/libraries/script-engine/src/v8/ScriptContextV8Wrapper.cpp
index 2b92a9ae8a..11e90b3a5a 100644
--- a/libraries/script-engine/src/v8/ScriptContextV8Wrapper.cpp
+++ b/libraries/script-engine/src/v8/ScriptContextV8Wrapper.cpp
@@ -111,6 +111,37 @@ QStringList ScriptContextV8Wrapper::backtrace() const {
     return backTrace;
 }
 
+int ScriptContextV8Wrapper::currentLineNumber() const {
+    auto isolate = _engine->getIsolate();
+    v8::Locker locker(isolate);
+    v8::Isolate::Scope isolateScope(isolate);
+    v8::HandleScope handleScope(isolate);
+    v8::Context::Scope contextScope(_context.Get(isolate));
+    v8::Local<v8::StackTrace> stackTrace = v8::StackTrace::CurrentStackTrace(isolate, 1);
+    if (stackTrace->GetFrameCount() > 0) {
+        v8::Local<v8::StackFrame> stackFrame = stackTrace->GetFrame(isolate, 0);
+        return stackFrame->GetLineNumber();
+    } else {
+        return -1;
+    }
+}
+
+QString ScriptContextV8Wrapper::currentFileName() const {
+    auto isolate = _engine->getIsolate();
+    v8::Locker locker(isolate);
+    v8::Isolate::Scope isolateScope(isolate);
+    v8::HandleScope handleScope(isolate);
+    v8::Context::Scope contextScope(_context.Get(isolate));
+    v8::Local<v8::StackTrace> stackTrace = v8::StackTrace::CurrentStackTrace(isolate, 1);
+    QStringList backTrace;
+    if (stackTrace->GetFrameCount() > 0) {
+        v8::Local<v8::StackFrame> stackFrame = stackTrace->GetFrame(isolate, 0);
+        return *v8::String::Utf8Value(isolate, stackFrame->GetScriptNameOrSourceURL());
+    } else {
+        return "";
+    }
+}
+
 ScriptValue ScriptContextV8Wrapper::callee() const {
     Q_ASSERT(false);
     //V8TODO
diff --git a/libraries/script-engine/src/v8/ScriptContextV8Wrapper.h b/libraries/script-engine/src/v8/ScriptContextV8Wrapper.h
index c410587c45..4512e72818 100644
--- a/libraries/script-engine/src/v8/ScriptContextV8Wrapper.h
+++ b/libraries/script-engine/src/v8/ScriptContextV8Wrapper.h
@@ -45,6 +45,13 @@ public: // ScriptContext implementation
     virtual int argumentCount() const override;
     virtual ScriptValue argument(int index) const override;
     virtual QStringList backtrace() const override;
+
+    // Name of the file in which message was generated. Empty string when no file name is available.
+    virtual int currentLineNumber() const override;
+
+    // Number of the line on which message was generated. -1 if there line number is not available.
+    virtual QString currentFileName() const override;
+
     virtual ScriptValue callee() const override;
     virtual ScriptEnginePointer engine() const override;
     virtual ScriptFunctionContextPointer functionContext() const override;
diff --git a/libraries/script-engine/src/v8/ScriptEngineV8.cpp b/libraries/script-engine/src/v8/ScriptEngineV8.cpp
index e4f6ad66f2..5a9dead93f 100644
--- a/libraries/script-engine/src/v8/ScriptEngineV8.cpp
+++ b/libraries/script-engine/src/v8/ScriptEngineV8.cpp
@@ -77,6 +77,17 @@ bool ScriptEngineV8::IS_THREADSAFE_INVOCATION(const QThread* thread, const QStri
     return false;
 }
 
+QString getFileNameFromTryCatch(v8::TryCatch &tryCatch, v8::Isolate *isolate, v8::Local<v8::Context> &context ) {
+    v8::Local<v8::Message> exceptionMessage = tryCatch.Message();
+    QString errorFileName;
+    auto resource = exceptionMessage->GetScriptResourceName();
+    v8::Local<v8::String> v8resourceString;
+    if (resource->ToString(context).ToLocal(&v8resourceString)) {
+        errorFileName = QString(*v8::String::Utf8Value(isolate, v8resourceString));
+    }
+    return errorFileName;
+}
+
 ScriptValue ScriptEngineV8::makeError(const ScriptValue& _other, const QString& type) {
     if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
         return nullValue();
@@ -307,7 +318,7 @@ void ScriptEngineV8::registerValue(const QString& valueName, V8ScriptValue value
     v8::Isolate::Scope isolateScope(_v8Isolate);
     v8::HandleScope handleScope(_v8Isolate);
     v8::Local<v8::Context> context = getContext();
-    v8::Context::Scope contextScope(getContext());
+    v8::Context::Scope contextScope(context);
     QStringList pathToValue = valueName.split(".");
     int partsToGo = pathToValue.length();
     v8::Local<v8::Object> partObject = context->Global();
@@ -370,17 +381,17 @@ void ScriptEngineV8::registerGlobalObject(const QString& name, QObject* object,
     Q_ASSERT(_v8Isolate->IsCurrent());
     v8::Local<v8::Context> context = getContext();
     v8::Context::Scope contextScope(context);
-    v8::Local<v8::Object> v8GlobalObject = getContext()->Global();
+    v8::Local<v8::Object> v8GlobalObject = context->Global();
     v8::Local<v8::String> v8Name = v8::String::NewFromUtf8(_v8Isolate, name.toStdString().c_str()).ToLocalChecked();
 
-    if (!v8GlobalObject->Get(getContext(), v8Name).IsEmpty()) {
+    if (!v8GlobalObject->Get(context, v8Name).IsEmpty()) {
         if (object) {
             V8ScriptValue value = ScriptObjectV8Proxy::newQObject(this, object, ScriptEngine::QtOwnership);
-            if(!v8GlobalObject->Set(getContext(), v8Name, value.get()).FromMaybe(false)) {
+            if(!v8GlobalObject->Set(context, v8Name, value.get()).FromMaybe(false)) {
                 Q_ASSERT(false);
             }
         } else {
-            if(!v8GlobalObject->Set(getContext(), v8Name, v8::Null(_v8Isolate)).FromMaybe(false)) {
+            if(!v8GlobalObject->Set(context, v8Name, v8::Null(_v8Isolate)).FromMaybe(false)) {
                 Q_ASSERT(false);
             }
         }
@@ -458,7 +469,8 @@ void ScriptEngineV8::registerGetterSetter(const QString& name, ScriptEngine::Fun
         v8::Locker locker(_v8Isolate);
         v8::Isolate::Scope isolateScope(_v8Isolate);
         v8::HandleScope handleScope(_v8Isolate);
-        v8::Context::Scope contextScope(getContext());
+        auto context = getContext();
+        v8::Context::Scope contextScope(context);
 
         ScriptValue setterFunction = newFunction(setter, 1);
         ScriptValue getterFunction = newFunction(getter);
@@ -482,7 +494,7 @@ void ScriptEngineV8::registerGetterSetter(const QString& name, ScriptEngine::Fun
                 } else {
                     v8ObjectToSetProperty = v8ParentObject;
                 }
-                    if (!v8ObjectToSetProperty->DefineProperty(getContext(), v8propertyName, propertyDescriptor).FromMaybe(false)) {
+                    if (!v8ObjectToSetProperty->DefineProperty(context, v8propertyName, propertyDescriptor).FromMaybe(false)) {
                     qCDebug(scriptengine_v8) << "DefineProperty failed for registerGetterSetter \"" << name << "\" for parent: \""
                                           << parent << "\"";
                 }
@@ -493,7 +505,7 @@ void ScriptEngineV8::registerGetterSetter(const QString& name, ScriptEngine::Fun
         } else {
             v8::Local<v8::String> v8propertyName =
                 v8::String::NewFromUtf8(_v8Isolate, name.toStdString().c_str()).ToLocalChecked();
-            if (!getContext()->Global()->DefineProperty(getContext(), v8propertyName, propertyDescriptor).FromMaybe(false)) {
+            if (!context->Global()->DefineProperty(context, v8propertyName, propertyDescriptor).FromMaybe(false)) {
                 qCDebug(scriptengine_v8) << "DefineProperty failed for registerGetterSetter \"" << name << "\" for global object";
             }
         }
@@ -527,7 +539,7 @@ void ScriptEngineV8::storeGlobalObjectContents() {
     v8::Local<v8::Object> globalMemberObjects = v8::Object::New(_v8Isolate);
 
     auto globalMemberNames = context->Global()->GetPropertyNames(context).ToLocalChecked();
-    for (size_t i = 0; i < globalMemberNames->Length(); i++) {
+    for (uint32_t i = 0; i < globalMemberNames->Length(); i++) {
         auto name = globalMemberNames->Get(context, i).ToLocalChecked();
         if(!globalMemberObjects->Set(context, name, context->Global()->Get(context, name).ToLocalChecked()).FromMaybe(false)) {
             Q_ASSERT(false);
@@ -557,7 +569,8 @@ ScriptValue ScriptEngineV8::evaluateInClosure(const ScriptValue& _closure,
     ScriptProgramV8Wrapper* unwrappedProgram;
 
     {
-        v8::Context::Scope contextScope(getContext());
+        auto context = getContext();
+        v8::Context::Scope contextScope(context);
         unwrappedProgram = ScriptProgramV8Wrapper::unwrap(_program);
         if (unwrappedProgram == nullptr) {
             _evaluatingCounter--;
@@ -588,7 +601,7 @@ ScriptValue ScriptEngineV8::evaluateInClosure(const ScriptValue& _closure,
         closureObject = v8::Local<v8::Object>::Cast(closure.constGet());
         qCDebug(scriptengine_v8) << "Closure object members:" << scriptValueDebugListMembersV8(closure);
         v8::Local<v8::Object> testObject = v8::Object::New(_v8Isolate);
-        if(!testObject->Set(getContext(), v8::String::NewFromUtf8(_v8Isolate, "test_value").ToLocalChecked(), closureObject).FromMaybe(false)) {
+        if(!testObject->Set(context, v8::String::NewFromUtf8(_v8Isolate, "test_value").ToLocalChecked(), closureObject).FromMaybe(false)) {
             Q_ASSERT(false);
         }
         qCDebug(scriptengine_v8) << "Test object members:" << scriptValueDebugListMembersV8(V8ScriptValue(this, testObject));
@@ -632,7 +645,7 @@ ScriptValue ScriptEngineV8::evaluateInClosure(const ScriptValue& _closure,
             // Since V8 cannot use arbitrary object as global object, objects from main global need to be copied to closure's global object
             auto globalObjectContents = _globalObjectContents.Get(_v8Isolate);
             auto globalMemberNames = globalObjectContents->GetPropertyNames(globalObjectContents->CreationContext()).ToLocalChecked();
-            for (size_t i = 0; i < globalMemberNames->Length(); i++) {
+            for (uint32_t i = 0; i < globalMemberNames->Length(); i++) {
                 auto name = globalMemberNames->Get(closureContext, i).ToLocalChecked();
                 if(!closureContext->Global()->Set(closureContext, name, globalObjectContents->Get(globalObjectContents->CreationContext(), name).ToLocalChecked()).FromMaybe(false)) {
                     Q_ASSERT(false);
@@ -643,7 +656,7 @@ ScriptValue ScriptEngineV8::evaluateInClosure(const ScriptValue& _closure,
             // Objects from closure need to be copied to global object too
             // V8TODO: I'm not sure which context to use with Get
             auto closureMemberNames = closureObject->GetPropertyNames(closureContext).ToLocalChecked();
-            for (size_t i = 0; i < closureMemberNames->Length(); i++) {
+            for (uint32_t i = 0; i < closureMemberNames->Length(); i++) {
                 auto name = closureMemberNames->Get(closureContext, i).ToLocalChecked();
                 if(!closureContext->Global()->Set(closureContext, name, closureObject->Get(closureContext, name).ToLocalChecked()).FromMaybe(false)) {
                     Q_ASSERT(false);
@@ -651,6 +664,11 @@ ScriptValue ScriptEngineV8::evaluateInClosure(const ScriptValue& _closure,
             }
             // "Script" API is context-dependent, so it needs to be recreated for each new context
             registerGlobalObject("Script", new ScriptManagerScriptingInterface(_manager), ScriptEngine::ScriptOwnership);
+            auto Script = globalObject().property("Script");
+            auto require = Script.property("require");
+            auto resolve = Script.property("_requireResolve");
+            require.setProperty("resolve", resolve, ScriptValue::ReadOnly | ScriptValue::Undeletable);
+            globalObject().setProperty("require", require, ScriptValue::ReadOnly | ScriptValue::Undeletable);
 
             // Script.require properties need to be copied, since that's where the Script.require cache is
             // Get source and destination Script.require objects
@@ -699,7 +717,7 @@ ScriptValue ScriptEngineV8::evaluateInClosure(const ScriptValue& _closure,
 
                 auto requireMemberNames =
                     oldRequireObject->GetPropertyNames(oldRequireObject->CreationContext()).ToLocalChecked();
-                for (size_t i = 0; i < requireMemberNames->Length(); i++) {
+                for (uint32_t i = 0; i < requireMemberNames->Length(); i++) {
                     auto name = requireMemberNames->Get(closureContext, i).ToLocalChecked();
                     v8::Local<v8::Value> oldObject;
                     if (!oldRequireObject->Get(oldRequireObject->CreationContext(), name).ToLocal(&oldObject)) {
@@ -724,7 +742,13 @@ ScriptValue ScriptEngineV8::evaluateInClosure(const ScriptValue& _closure,
                     + "tryCatch details:" + formatErrorMessageFromTryCatch(tryCatch);
                 v8Result = v8::Null(_v8Isolate);
                 if (_manager) {
-                    _manager->scriptErrorMessage(errorMessage);
+                    v8::Local<v8::Message> exceptionMessage = tryCatch.Message();
+                    int errorLineNumber = -1;
+                    if (!exceptionMessage.IsEmpty()) {
+                        errorLineNumber = exceptionMessage->GetLineNumber(closureContext).FromJust();
+                    }
+                    _manager->scriptErrorMessage(errorMessage, getFileNameFromTryCatch(tryCatch, _v8Isolate, closureContext),
+                                                          errorLineNumber);
                 } else {
                     qWarning(scriptengine_v8) << errorMessage;
                 }
@@ -770,15 +794,22 @@ ScriptValue ScriptEngineV8::evaluate(const QString& sourceCode, const QString& f
     v8::Locker locker(_v8Isolate);
     v8::Isolate::Scope isolateScope(_v8Isolate);
     v8::HandleScope handleScope(_v8Isolate);
-    v8::Context::Scope contextScope(getContext());
+    auto context = getContext();
+    v8::Context::Scope contextScope(context);
     v8::ScriptOrigin scriptOrigin(getIsolate(), v8::String::NewFromUtf8(getIsolate(), fileName.toStdString().c_str()).ToLocalChecked());
     v8::Local<v8::Script> script;
     {
         v8::TryCatch tryCatch(getIsolate());
-        if (!v8::Script::Compile(getContext(), v8::String::NewFromUtf8(getIsolate(), sourceCode.toStdString().c_str()).ToLocalChecked(), &scriptOrigin).ToLocal(&script)) {
+        if (!v8::Script::Compile(context, v8::String::NewFromUtf8(getIsolate(), sourceCode.toStdString().c_str()).ToLocalChecked(), &scriptOrigin).ToLocal(&script)) {
             QString errorMessage(QString("Error while compiling script: \"") + fileName + QString("\" ") + formatErrorMessageFromTryCatch(tryCatch));
             if (_manager) {
-                _manager->scriptErrorMessage(errorMessage);
+                v8::Local<v8::Message> exceptionMessage = tryCatch.Message();
+                int errorLineNumber = -1;
+                if (!exceptionMessage.IsEmpty()) {
+                    errorLineNumber = exceptionMessage->GetLineNumber(context).FromJust();
+                }
+                _manager->scriptErrorMessage(errorMessage, getFileNameFromTryCatch(tryCatch, _v8Isolate, context),
+                                                      errorLineNumber);
             } else {
                 qDebug(scriptengine_v8) << errorMessage;
             }
@@ -790,13 +821,19 @@ ScriptValue ScriptEngineV8::evaluate(const QString& sourceCode, const QString& f
 
     v8::Local<v8::Value> result;
     v8::TryCatch tryCatchRun(getIsolate());
-    if (!script->Run(getContext()).ToLocal(&result)) {
+    if (!script->Run(context).ToLocal(&result)) {
         Q_ASSERT(tryCatchRun.HasCaught());
         auto runError = tryCatchRun.Message();
         ScriptValue errorValue(new ScriptValueV8Wrapper(this, V8ScriptValue(this, runError->Get())));
         QString errorMessage(QString("Running script: \"") + fileName + QString("\" ") + formatErrorMessageFromTryCatch(tryCatchRun));
         if (_manager) {
-            _manager->scriptErrorMessage(errorMessage);
+            v8::Local<v8::Message> exceptionMessage = tryCatchRun.Message();
+            int errorLineNumber = -1;
+            if (!exceptionMessage.IsEmpty()) {
+                errorLineNumber = exceptionMessage->GetLineNumber(context).FromJust();
+            }
+            _manager->scriptErrorMessage(errorMessage, getFileNameFromTryCatch(tryCatchRun, _v8Isolate, context),
+                                                  errorLineNumber);
         } else {
             qDebug(scriptengine_v8) << errorMessage;
         }
@@ -829,7 +866,8 @@ void ScriptEngineV8::setUncaughtException(const v8::TryCatch &tryCatch, const QS
     v8::Locker locker(_v8Isolate);
     v8::Isolate::Scope isolateScope(_v8Isolate);
     v8::HandleScope handleScope(_v8Isolate);
-    v8::Context::Scope contextScope(getContext());
+    v8::Local<v8::Context> context = getContext();
+    v8::Context::Scope contextScope(context);
     QString result("");
 
     QString errorMessage = "";
@@ -844,10 +882,10 @@ void ScriptEngineV8::setUncaughtException(const v8::TryCatch &tryCatch, const QS
 
     v8::Local<v8::Message> exceptionMessage = tryCatch.Message();
     if (!exceptionMessage.IsEmpty()) {
-        ex->errorLine = exceptionMessage->GetLineNumber(getContext()).FromJust();
-        ex->errorColumn = exceptionMessage->GetStartColumn(getContext()).FromJust();
+        ex->errorLine = exceptionMessage->GetLineNumber(context).FromJust();
+        ex->errorColumn = exceptionMessage->GetStartColumn(context).FromJust();
         v8::Local<v8::Value> backtraceV8String;
-        if (tryCatch.StackTrace(getContext()).ToLocal(&backtraceV8String)) {
+        if (tryCatch.StackTrace(context).ToLocal(&backtraceV8String)) {
             if (backtraceV8String->IsString()) {
                 if (v8::Local<v8::String>::Cast(backtraceV8String)->Length() > 0) {
                     v8::String::Utf8Value backtraceUtf8Value(getIsolate(), backtraceV8String);
@@ -875,7 +913,8 @@ QString ScriptEngineV8::formatErrorMessageFromTryCatch(v8::TryCatch &tryCatch) {
     v8::Locker locker(_v8Isolate);
     v8::Isolate::Scope isolateScope(_v8Isolate);
     v8::HandleScope handleScope(_v8Isolate);
-    v8::Context::Scope contextScope(getContext());
+    auto context = getContext();
+    v8::Context::Scope contextScope(context);
     QString result("");
     int errorColumnNumber = 0;
     int errorLineNumber = 0;
@@ -885,10 +924,10 @@ QString ScriptEngineV8::formatErrorMessageFromTryCatch(v8::TryCatch &tryCatch) {
     errorMessage = QString(*utf8Value);
     v8::Local<v8::Message> exceptionMessage = tryCatch.Message();
     if (!exceptionMessage.IsEmpty()) {
-        errorLineNumber = exceptionMessage->GetLineNumber(getContext()).FromJust();
-        errorColumnNumber = exceptionMessage->GetStartColumn(getContext()).FromJust();
+        errorLineNumber = exceptionMessage->GetLineNumber(context).FromJust();
+        errorColumnNumber = exceptionMessage->GetStartColumn(context).FromJust();
         v8::Local<v8::Value> backtraceV8String;
-        if (tryCatch.StackTrace(getContext()).ToLocal(&backtraceV8String)) {
+        if (tryCatch.StackTrace(context).ToLocal(&backtraceV8String)) {
             if (backtraceV8String->IsString()) {
                 if (v8::Local<v8::String>::Cast(backtraceV8String)->Length() > 0) {
                     v8::String::Utf8Value backtraceUtf8Value(getIsolate(), backtraceV8String);
@@ -1000,7 +1039,8 @@ Q_INVOKABLE ScriptValue ScriptEngineV8::evaluate(const ScriptProgramPointer& pro
         v8::Locker locker(_v8Isolate);
         v8::Isolate::Scope isolateScope(_v8Isolate);
         v8::HandleScope handleScope(_v8Isolate);
-        v8::Context::Scope contextScope(getContext());
+        auto context = getContext();
+        v8::Context::Scope contextScope(context);
         ScriptProgramV8Wrapper* unwrapped = ScriptProgramV8Wrapper::unwrap(program);
         if (!unwrapped) {
             setUncaughtEngineException("Could not unwrap program", "Compile error");
@@ -1020,7 +1060,7 @@ Q_INVOKABLE ScriptValue ScriptEngineV8::evaluate(const ScriptProgramPointer& pro
             const V8ScriptProgram& v8Program = unwrapped->toV8Value();
 
             v8::TryCatch tryCatchRun(getIsolate());
-            if (!v8Program.constGet()->Run(getContext()).ToLocal(&result)) {
+            if (!v8Program.constGet()->Run(context).ToLocal(&result)) {
                 Q_ASSERT(tryCatchRun.HasCaught());
                 auto runError = tryCatchRun.Message();
                 errorValue = ScriptValue(new ScriptValueV8Wrapper(this, V8ScriptValue(this, runError->Get())));
@@ -1252,7 +1292,8 @@ ScriptValue ScriptEngineV8::newFunction(ScriptEngine::FunctionSignature fun, int
     v8::Locker locker(_v8Isolate);
     v8::Isolate::Scope isolateScope(_v8Isolate);
     v8::HandleScope handleScope(_v8Isolate);
-    v8::Context::Scope contextScope(getContext());
+    auto context = getContext();
+    v8::Context::Scope contextScope(context);
 
     auto v8FunctionCallback = [](const v8::FunctionCallbackInfo<v8::Value>& info) {
         //V8TODO: is using GetCurrentContext ok, or context wrapper needs to be added?
@@ -1276,10 +1317,10 @@ ScriptValue ScriptEngineV8::newFunction(ScriptEngine::FunctionSignature fun, int
         }
     };
     auto functionDataTemplate = getFunctionDataTemplate();
-    auto functionData = functionDataTemplate->NewInstance(getContext()).ToLocalChecked();
+    auto functionData = functionDataTemplate->NewInstance(context).ToLocalChecked();
     functionData->SetAlignedPointerInInternalField(0, reinterpret_cast<void*>(fun));
     functionData->SetAlignedPointerInInternalField(1, reinterpret_cast<void*>(this));
-    auto v8Function = v8::Function::New(getContext(), v8FunctionCallback, functionData, length).ToLocalChecked();
+    auto v8Function = v8::Function::New(context, v8FunctionCallback, functionData, length).ToLocalChecked();
     V8ScriptValue result(this, v8Function);
     return ScriptValue(new ScriptValueV8Wrapper(this, std::move(result)));
 }
@@ -1293,11 +1334,12 @@ bool ScriptEngineV8::setProperty(const char* name, const QVariant& value) {
     v8::Locker locker(_v8Isolate);
     v8::Isolate::Scope isolateScope(_v8Isolate);
     v8::HandleScope handleScope(_v8Isolate);
-    v8::Context::Scope contextScope(getContext());
-    v8::Local<v8::Object> global = getContext()->Global();
+    v8::Local<v8::Context> context = getContext();
+    v8::Context::Scope contextScope(context);
+    v8::Local<v8::Object> global = context->Global();
     auto v8Name = v8::String::NewFromUtf8(getIsolate(), name).ToLocalChecked();
     V8ScriptValue v8Value = castVariantToValue(value);
-    return global->Set(getContext(), v8Name, v8Value.get()).FromMaybe(false);
+    return global->Set(context, v8Name, v8Value.get()).FromMaybe(false);
 }
 
 void ScriptEngineV8::setProcessEventsInterval(int interval) {
@@ -1411,10 +1453,11 @@ void ScriptEngineV8::compileTest() {
     v8::Locker locker(_v8Isolate);
     v8::Isolate::Scope isolateScope(_v8Isolate);
     v8::HandleScope handleScope(_v8Isolate);
-    v8::Context::Scope contextScope(getContext());
+    auto context = getContext();
+    v8::Context::Scope contextScope(context);
     v8::Local<v8::Script> script;
     v8::ScriptOrigin scriptOrigin(getIsolate(), v8::String::NewFromUtf8(getIsolate(),"test").ToLocalChecked());
-    if (v8::Script::Compile(getContext(), v8::String::NewFromUtf8(getIsolate(), "print(\"hello world\");").ToLocalChecked(), &scriptOrigin).ToLocal(&script)) {
+    if (v8::Script::Compile(context, v8::String::NewFromUtf8(getIsolate(), "print(\"hello world\");").ToLocalChecked(), &scriptOrigin).ToLocal(&script)) {
         qCDebug(scriptengine_v8) << "Compile test successful";
     } else {
         qCDebug(scriptengine_v8) << "Compile test failed";
@@ -1436,14 +1479,15 @@ QString ScriptEngineV8::scriptValueDebugListMembersV8(const V8ScriptValue &v8Val
     v8::Locker locker(_v8Isolate);
     v8::Isolate::Scope isolateScope(_v8Isolate);
     v8::HandleScope handleScope(_v8Isolate);
-    v8::Context::Scope contextScope(getContext());
+    v8::Local<v8::Context> context = getContext();
+    v8::Context::Scope contextScope(context);
 
     QString membersString("");
     if (v8Value.constGet()->IsObject()) {
         v8::Local<v8::String> membersStringV8;
         v8::Local<v8::Object> object = v8::Local<v8::Object>::Cast(v8Value.constGet());
-        auto names = object->GetPropertyNames(getContext()).ToLocalChecked();
-        if (v8::JSON::Stringify(getContext(), names).ToLocal(&membersStringV8)) {
+        auto names = object->GetPropertyNames(context).ToLocalChecked();
+        if (v8::JSON::Stringify(context, names).ToLocal(&membersStringV8)) {
             membersString = QString(*v8::String::Utf8Value(_v8Isolate, membersStringV8));
         }
         membersString = QString(*v8::String::Utf8Value(_v8Isolate, membersStringV8));
@@ -1457,16 +1501,17 @@ QString ScriptEngineV8::scriptValueDebugDetailsV8(const V8ScriptValue &v8Value)
     v8::Locker locker(_v8Isolate);
     v8::Isolate::Scope isolateScope(_v8Isolate);
     v8::HandleScope handleScope(_v8Isolate);
-    v8::Context::Scope contextScope(getContext());
+    v8::Local<v8::Context> context = getContext();
+    v8::Context::Scope contextScope(context);
 
     QString parentValueQString("");
     v8::Local<v8::String> parentValueString;
-    if (v8Value.constGet()->ToDetailString(getContext()).ToLocal(&parentValueString)) {
+    if (v8Value.constGet()->ToDetailString(context).ToLocal(&parentValueString)) {
         parentValueQString = QString(*v8::String::Utf8Value(_v8Isolate, parentValueString));
     }
     QString JSONQString;
     v8::Local<v8::String> JSONString;
-    if (v8::JSON::Stringify(getContext(), v8Value.constGet()).ToLocal(&JSONString)) {
+    if (v8::JSON::Stringify(context, v8Value.constGet()).ToLocal(&JSONString)) {
         JSONQString = QString(*v8::String::Utf8Value(_v8Isolate, JSONString));
     }
     return parentValueQString + QString(" JSON: ") + JSONQString;
diff --git a/libraries/script-engine/src/v8/ScriptEngineV8.h b/libraries/script-engine/src/v8/ScriptEngineV8.h
index 5badba271e..02352c9ed7 100644
--- a/libraries/script-engine/src/v8/ScriptEngineV8.h
+++ b/libraries/script-engine/src/v8/ScriptEngineV8.h
@@ -309,6 +309,8 @@ private:
     ScriptEngineV8* _engine;
 };
 
+QString getFileNameFromTryCatch(v8::TryCatch &tryCatch, v8::Isolate *isolate, v8::Local<v8::Context> &context );
+
 #include "V8Types.h"
 
 #endif  // hifi_ScriptEngineV8_h
diff --git a/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp b/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp
index 8dead23ddb..211da7582f 100644
--- a/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp
+++ b/libraries/script-engine/src/v8/ScriptEngineV8_cast.cpp
@@ -725,12 +725,12 @@ V8ScriptValue ScriptEngineV8::castVariantToValue(const QVariant& val) {
         case QMetaType::QDateTime:
             {
                 double timeMs = val.value<QDateTime>().currentMSecsSinceEpoch();
-                return V8ScriptValue(this, v8::Date::New(getContext(), timeMs).ToLocalChecked());
+                return V8ScriptValue(this, v8::Date::New(context, timeMs).ToLocalChecked());
             }
         case QMetaType::QDate:
             {
                 double timeMs = val.value<QDate>().startOfDay().currentMSecsSinceEpoch();
-                return V8ScriptValue(this, v8::Date::New(getContext(), timeMs).ToLocalChecked());
+                return V8ScriptValue(this, v8::Date::New(context, timeMs).ToLocalChecked());
             }
         default:
             // check to see if this is a pointer to a QObject-derived object
diff --git a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp
index 177eeb259f..7c3fd07e9b 100644
--- a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp
+++ b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.cpp
@@ -56,6 +56,13 @@ public:  // ScriptContext implementation
     virtual int argumentCount() const override { return _parent->argumentCount(); }
     virtual ScriptValue argument(int index) const override { return _parent->argument(index); }
     virtual QStringList backtrace() const override { return _parent->backtrace(); }
+
+    // Name of the file in which message was generated. Empty string when no file name is available.
+    virtual int currentLineNumber() const override { return _parent->currentLineNumber(); }
+
+    // Number of the line on which message was generated. -1 if there line number is not available.
+    virtual QString currentFileName() const override { return _parent->currentFileName(); }
+
     virtual ScriptValue callee() const override { return _parent->callee(); }
     virtual ScriptEnginePointer engine() const override { return _parent->engine(); }
     virtual ScriptFunctionContextPointer functionContext() const override { return _parent->functionContext(); }
@@ -127,6 +134,23 @@ V8ScriptValue ScriptObjectV8Proxy::newQObject(ScriptEngineV8* engine, QObject* o
         QPointer<ScriptEngineV8> enginePtr = engine;
         object->connect(object, &QObject::destroyed, engine, [enginePtr, object]() {
             if (!enginePtr) return;
+            // Lookup needs to be done twice, because _qobjectWrapperMapProtect must not be locked when disconnecting signals
+            QSharedPointer<ScriptObjectV8Proxy> proxy;
+            {
+                QMutexLocker guard(&enginePtr->_qobjectWrapperMapProtect);
+                auto lookupV8 = enginePtr->_qobjectWrapperMapV8.find(object);
+                if (lookupV8 != enginePtr->_qobjectWrapperMapV8.end()) {
+                    proxy = lookupV8.value();
+                }
+            }
+            if (proxy) {
+                for (auto signal : proxy->_signalInstances) {
+                    if (signal) {
+                        signal->disconnectAllScriptSignalProxies();
+                    }
+                }
+            }
+
             QMutexLocker guard(&enginePtr->_qobjectWrapperMapProtect);
             ScriptEngineV8::ObjectWrapperMap::iterator lookup = enginePtr->_qobjectWrapperMap.find(object);
             if (lookup != enginePtr->_qobjectWrapperMap.end()) {
@@ -188,7 +212,13 @@ ScriptObjectV8Proxy* ScriptObjectV8Proxy::unwrapProxy(v8::Isolate* isolate, v8::
         qCDebug(scriptengine_v8) << "Cannot unwrap proxy - internal fields don't point to object proxy";
         return nullptr;
     }
-    return reinterpret_cast<ScriptObjectV8Proxy*>(v8Object->GetAlignedPointerFromInternalField(1));
+
+    ScriptObjectV8Proxy* proxy = reinterpret_cast<ScriptObjectV8Proxy*>(v8Object->GetAlignedPointerFromInternalField(1));
+    if (proxy) {
+        Q_ASSERT(!proxy->_wasDestroyed);
+    }
+
+    return proxy;
 }
 
 QObject* ScriptObjectV8Proxy::unwrap(const V8ScriptValue& val) {
@@ -197,6 +227,12 @@ QObject* ScriptObjectV8Proxy::unwrap(const V8ScriptValue& val) {
 }
 
 ScriptObjectV8Proxy::~ScriptObjectV8Proxy() {
+    for (auto signal : _signalInstances) {
+        if (signal) {
+            signal->disconnectAllScriptSignalProxies();
+        }
+    }
+    _wasDestroyed = true;
     if (_ownsObject) {
         auto isolate = _engine->getIsolate();
         v8::Locker locker(isolate);
@@ -231,7 +267,8 @@ void ScriptObjectV8Proxy::investigate() {
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(_engine->getIsolate());
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
 
     const QMetaObject* metaObject = qobject->metaObject();
 
@@ -348,7 +385,7 @@ void ScriptObjectV8Proxy::investigate() {
         }
     }
 
-    v8::Local<v8::Object> v8Object = objectTemplate->NewInstance(_engine->getContext()).ToLocalChecked();
+    v8::Local<v8::Object> v8Object = objectTemplate->NewInstance(context).ToLocalChecked();
 
     v8Object->SetAlignedPointerInInternalField(0, const_cast<void*>(internalPointsToQObjectProxy));
     v8Object->SetAlignedPointerInInternalField(1, reinterpret_cast<void*>(this));
@@ -366,7 +403,7 @@ void ScriptObjectV8Proxy::investigate() {
     for (auto i = _methods.begin(); i != _methods.end(); i++) {
         V8ScriptValue method = ScriptMethodV8Proxy::newMethod(_engine, qobject, V8ScriptValue(_engine, v8Object),
                                                               i.value().methods, i.value().numMaxParams);
-        if(!propertiesObject->Set(_engine->getContext(), v8::String::NewFromUtf8(isolate, i.value().name.toStdString().c_str()).ToLocalChecked(), method.get()).FromMaybe(false)) {
+        if(!propertiesObject->Set(context, v8::String::NewFromUtf8(isolate, i.value().name.toStdString().c_str()).ToLocalChecked(), method.get()).FromMaybe(false)) {
             Q_ASSERT(false);
         }
     }
@@ -555,7 +592,7 @@ v8::Local<v8::Array> ScriptObjectV8Proxy::getPropertyNames() {
     v8::Isolate::Scope isolateScope(isolate);
     v8::EscapableHandleScope handleScope(_engine->getIsolate());
     auto context = _engine->getContext();
-    v8::Context::Scope contextScope(_engine->getContext());
+    v8::Context::Scope contextScope(context);
 
     //V8TODO: this is really slow. It could be cached if this is called often.
     v8::Local<v8::Array> properties = v8::Array::New(isolate, _props.size() + _methods.size() + _signals.size());
@@ -587,7 +624,8 @@ V8ScriptValue ScriptObjectV8Proxy::property(const V8ScriptValue& object, const V
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     QObject* qobject = _object;
     if (!qobject) {
         _engine->getIsolate()->ThrowError("Referencing deleted native object");
@@ -621,7 +659,7 @@ V8ScriptValue ScriptObjectV8Proxy::property(const V8ScriptValue& object, const V
                 }
             } //V8TODO: is new method created during every call? It needs to be cached instead
             v8::Local<v8::Value> property;
-            if(_v8Object.Get(isolate)->GetInternalField(2).As<v8::Object>()->Get(_engine->getContext(), name.constGet()).ToLocal(&property)) {
+            if(_v8Object.Get(isolate)->GetInternalField(2).As<v8::Object>()->Get(context, name.constGet()).ToLocal(&property)) {
                 if (!property->IsUndefined()) {
                     return V8ScriptValue(_engine, property);
                 }
@@ -698,9 +736,10 @@ ScriptVariantV8Proxy::ScriptVariantV8Proxy(ScriptEngineV8* engine, const QVarian
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(engine->getContext());
+    auto context = engine->getContext();
+    v8::Context::Scope contextScope(context);
     auto variantDataTemplate = _engine->getVariantDataTemplate();
-    auto variantData = variantDataTemplate->NewInstance(engine->getContext()).ToLocalChecked();
+    auto variantData = variantDataTemplate->NewInstance(context).ToLocalChecked();
     variantData->SetAlignedPointerInInternalField(0, const_cast<void*>(internalPointsToQVariantInProxy));
     // Internal field doesn't point directly to QVariant, because then alignment would need to be guaranteed in all compilers
     variantData->SetAlignedPointerInInternalField(1, reinterpret_cast<void*>(this));
@@ -723,7 +762,8 @@ V8ScriptValue ScriptVariantV8Proxy::newVariant(ScriptEngineV8* engine, const QVa
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(engine->getContext());
+    auto context = engine->getContext();
+    v8::Context::Scope contextScope(context);
     ScriptObjectV8Proxy* protoProxy = ScriptObjectV8Proxy::unwrapProxy(proto);
     if (!protoProxy) {
         Q_ASSERT(protoProxy);
@@ -734,7 +774,7 @@ V8ScriptValue ScriptVariantV8Proxy::newVariant(ScriptEngineV8* engine, const QVa
     auto proxy = new ScriptVariantV8Proxy(engine, variant, proto, protoProxy);
 
     auto variantProxyTemplate = engine->getVariantProxyTemplate();
-    auto variantProxy = variantProxyTemplate->NewInstance(engine->getContext()).ToLocalChecked();
+    auto variantProxy = variantProxyTemplate->NewInstance(context).ToLocalChecked();
     variantProxy->SetAlignedPointerInInternalField(0, const_cast<void*>(internalPointsToQVariantProxy));
     variantProxy->SetAlignedPointerInInternalField(1, reinterpret_cast<void*>(proxy));
     return V8ScriptValue(engine, variantProxy);
@@ -912,12 +952,13 @@ V8ScriptValue ScriptMethodV8Proxy::newMethod(ScriptEngineV8* engine, QObject* ob
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(engine->getContext());
+    auto context = engine->getContext();
+    v8::Context::Scope contextScope(context);
     auto methodDataTemplate = engine->getMethodDataTemplate();
-    auto methodData = methodDataTemplate->NewInstance(engine->getContext()).ToLocalChecked();
+    auto methodData = methodDataTemplate->NewInstance(context).ToLocalChecked();
     methodData->SetAlignedPointerInInternalField(0, const_cast<void*>(internalPointsToMethodProxy));
     methodData->SetAlignedPointerInInternalField(1, reinterpret_cast<void*>(new ScriptMethodV8Proxy(engine, object, lifetime, metas, numMaxParams)));
-    auto v8Function = v8::Function::New(engine->getContext(), callback, methodData, numMaxParams).ToLocalChecked();
+    auto v8Function = v8::Function::New(context, callback, methodData, numMaxParams).ToLocalChecked();
     return V8ScriptValue(engine, v8Function);
 }
 
@@ -964,7 +1005,8 @@ void ScriptMethodV8Proxy::call(const v8::FunctionCallbackInfo<v8::Value>& argume
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
     ContextScopeV8 contextScopeV8(_engine);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     QObject* qobject = _object;
     if (!qobject) {
         isolate->ThrowError("Referencing deleted native object");
@@ -1027,7 +1069,7 @@ void ScriptMethodV8Proxy::call(const v8::FunctionCallbackInfo<v8::Value>& argume
                 } else {
                     qVarArgLists[i].append(varArgVal);
                     const QVariant& converted = qVarArgLists[i].back();
-                    conversionPenaltyScore = _engine->computeCastPenalty(V8ScriptValue(_engine, argVal), methodArgTypeId);
+                    conversionPenaltyScore += _engine->computeCastPenalty(V8ScriptValue(_engine, argVal), methodArgTypeId);
 
                     // a lot of type conversion assistance thanks to https://stackoverflow.com/questions/28457819/qt-invoke-method-with-qvariant
                     // A const_cast is needed because calling data() would detach the QVariant.
@@ -1057,7 +1099,7 @@ void ScriptMethodV8Proxy::call(const v8::FunctionCallbackInfo<v8::Value>& argume
 
     if (isValidMetaSelected) {
         // V8TODO: is this the correct wrapper?
-        ScriptContextV8Wrapper ourContext(_engine, &arguments, _engine->getContext(),
+        ScriptContextV8Wrapper ourContext(_engine, &arguments, context,
                                           _engine->currentContext()->parentContext());
         ScriptContextGuard guard(&ourContext);
         const QMetaMethod& meta = _metas[bestMeta];
@@ -1153,16 +1195,21 @@ ScriptSignalV8Proxy::ScriptSignalV8Proxy(ScriptEngineV8* engine, QObject* object
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     _objectLifetime.Reset(isolate, lifetime.get());
     _objectLifetime.SetWeak(this, weakHandleCallback, v8::WeakCallbackType::kParameter);
-    _v8Context.Reset(isolate, _engine->getContext());
+    _v8Context.Reset(isolate, context);
     _engine->_signalProxySetLock.lockForWrite();
     _engine->_signalProxySet.insert(this);
     _engine->_signalProxySetLock.unlock();
 }
 
 ScriptSignalV8Proxy::~ScriptSignalV8Proxy() {
+    if (!_cleanup) {
+        disconnectAllScriptSignalProxies();
+    }
+
     auto isolate = _engine->getIsolate();
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
@@ -1262,13 +1309,20 @@ int ScriptSignalV8Proxy::qt_metacall(QMetaObject::Call call, int id, void** argu
                 }
 
                 v8::TryCatch tryCatch(isolate);
-                callback->Call(functionContext, v8This, numArgs, args);
+                auto maybeResult = callback->Call(functionContext, v8This, numArgs, args);
+                Q_UNUSED(maybeResult); // Signals don't have return values
                 if (tryCatch.HasCaught()) {
                     QString errorMessage(QString("Signal proxy ") + fullName() + " connection call failed: \""
                                           + _engine->formatErrorMessageFromTryCatch(tryCatch)
                                           + "\nThis provided: " + QString::number(conn.thisValue.get()->IsObject()));
+                    v8::Local<v8::Message> exceptionMessage = tryCatch.Message();
+                    int errorLineNumber = -1;
+                    if (!exceptionMessage.IsEmpty()) {
+                        errorLineNumber = exceptionMessage->GetLineNumber(context).FromJust();
+                    }
                     if (_engine->_manager) {
-                        _engine->_manager->scriptErrorMessage(errorMessage);
+                        _engine->_manager->scriptErrorMessage(errorMessage, getFileNameFromTryCatch(tryCatch, isolate, context),
+                                                              errorLineNumber);
                     } else {
                         qDebug(scriptengine_v8) << errorMessage;
                     }
@@ -1313,7 +1367,8 @@ void ScriptSignalV8Proxy::connect(ScriptValue arg0, ScriptValue arg1) {
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     QObject* qobject = _object;
     if (!qobject) {
         isolate->ThrowError("Referencing deleted native object");
@@ -1362,7 +1417,7 @@ void ScriptSignalV8Proxy::connect(ScriptValue arg0, ScriptValue arg1) {
     v8::Local<v8::Value> destData;
     // V8TODO: I'm not sure which context to use here
     //auto destFunctionContext = destFunction->CreationContext();
-    auto destFunctionContext = _engine->getContext();
+    auto destFunctionContext = context;
     Q_ASSERT(thisObject().isObject());
     V8ScriptValue v8ThisObject = ScriptValueV8Wrapper::fullUnwrap(_engine, thisObject());
     Q_ASSERT(ScriptObjectV8Proxy::unwrapProxy(v8ThisObject));
@@ -1387,18 +1442,23 @@ void ScriptSignalV8Proxy::connect(ScriptValue arg0, ScriptValue arg1) {
                 Q_ASSERT(ScriptObjectV8Proxy::unwrapProxy(v8EntryObject));
                 // For debugging
                 ScriptSignalV8Proxy* entryProxy = dynamic_cast<ScriptSignalV8Proxy*>(ScriptObjectV8Proxy::unwrapProxy(v8EntryObject)->toQObject());
-                Q_ASSERT(thisProxy);
+                Q_ASSERT(entryProxy);
                 qCDebug(scriptengine_v8) << "ScriptSignalV8Proxy::connect: entry proxy: " << entryProxy->fullName();
             }
             if (!newArray->Set(destFunctionContext, idx, entry).FromMaybe(false)) {
                 Q_ASSERT(false);
             }
+            if (entry->StrictEquals(v8ThisObject.get())) {
+                foundIt = true;
+            }
         }
-        if (!newArray->Set(destFunctionContext, length, v8ThisObject.get()).FromMaybe(false)) {
-            Q_ASSERT(false);
-        }
-        if (!destFunction->Set(destFunctionContext, destDataName, newArray).FromMaybe(false)) {
-            Q_ASSERT(false);
+        if (!foundIt) {
+            if (!newArray->Set(destFunctionContext, length, v8ThisObject.get()).FromMaybe(false)) {
+                Q_ASSERT(false);
+            }
+            if (!destFunction->Set(destFunctionContext, destDataName, newArray).FromMaybe(false)) {
+                Q_ASSERT(false);
+            }
         }
     } else {
         v8::Local<v8::Array> newArray = v8::Array::New(isolate, 1);
@@ -1428,14 +1488,17 @@ void ScriptSignalV8Proxy::connect(ScriptValue arg0, ScriptValue arg1) {
 void ScriptSignalV8Proxy::disconnect(ScriptValue arg0, ScriptValue arg1) {
     QObject* qobject = _object;
     v8::Isolate *isolate = _engine->getIsolate();
-    if (!qobject) {
-        isolate->ThrowError("Referencing deleted native object");
-        return;
+    if (!_cleanup) {
+        if (!qobject) {
+            isolate->ThrowError("Referencing deleted native object");
+            return;
+        }
     }
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
 
     // untangle the arguments
     V8ScriptValue callback(_engine, v8::Null(isolate));
@@ -1480,15 +1543,11 @@ void ScriptSignalV8Proxy::disconnect(ScriptValue arg0, ScriptValue arg1) {
     v8::Local<v8::String> destDataName = v8::String::NewFromUtf8(isolate, "__data__").ToLocalChecked();
     v8::Local<v8::Value> destData;
 
-    //auto destFunctionContext = destFunction->CreationContext();
-    auto destFunctionContext = _engine->getContext();
-    Q_ASSERT(thisObject().isObject());
-    V8ScriptValue v8ThisObject = ScriptValueV8Wrapper::fullUnwrap(_engine, thisObject());
-    Q_ASSERT(ScriptObjectV8Proxy::unwrapProxy(v8ThisObject));
-    // For debugging
-    ScriptSignalV8Proxy* thisProxy = dynamic_cast<ScriptSignalV8Proxy*>(ScriptObjectV8Proxy::unwrapProxy(v8ThisObject)->toQObject());
-    Q_ASSERT(thisProxy);
-    //qCDebug(scriptengine_v8) << "ScriptSignalV8Proxy::disconnect: " << thisProxy->fullName() << " fullName: " << fullName();
+    auto destFunctionContext = context;
+    ScriptEngine::QObjectWrapOptions options = ScriptEngine::ExcludeSuperClassContents |
+                                               ScriptEngine::PreferExistingWrapperObject;
+    // It's not necessarily new, newQObject looks for it first in object wrapper map, and for already existing signal it should be found there
+    V8ScriptValue v8ThisObject = ScriptObjectV8Proxy::newQObject(_engine, this, ScriptEngine::ScriptOwnership, options);
     if (!destFunction->Get(destFunctionContext, destDataName).ToLocal(&destData)) {
         Q_ASSERT(false);
     }
@@ -1496,24 +1555,18 @@ void ScriptSignalV8Proxy::disconnect(ScriptValue arg0, ScriptValue arg1) {
         v8::Local<v8::Array> destArray = v8::Local<v8::Array>::Cast(destData);
         int length = destArray->Length();
         v8::Local<v8::Array> newArray = v8::Array::New(isolate, length - 1);
-        bool foundIt = false;
+        int findCounter = 0;
         int newIndex = 0;
         for (int idx = 0; idx < length; ++idx) {
             v8::Local<v8::Value> entry = destArray->Get(destFunctionContext, idx).ToLocalChecked();
             // For debugging:
             {
-                //qCDebug(scriptengine_v8) << "ScriptSignalV8Proxy::disconnect: entry details: " << _engine->scriptValueDebugDetailsV8(V8ScriptValue(_engine, entry))
-                //         << " Array: " << _engine->scriptValueDebugDetailsV8(V8ScriptValue(_engine, destArray));
                 Q_ASSERT(entry->IsObject());
                 V8ScriptValue v8EntryObject(_engine, entry);
                 Q_ASSERT(ScriptObjectV8Proxy::unwrapProxy(v8EntryObject));
-                // For debugging
-                //ScriptSignalV8Proxy* entryProxy = dynamic_cast<ScriptSignalV8Proxy*>(ScriptObjectV8Proxy::unwrapProxy(v8EntryObject)->toQObject());
-                Q_ASSERT(thisProxy);
-                //qCDebug(scriptengine_v8) << "ScriptSignalV8Proxy::disconnect: entry proxy: " << entryProxy->fullName();
             }
             if (entry->StrictEquals(v8ThisObject.get())) {
-                foundIt = true;
+                findCounter++;
             } else {
                 if (!newArray->Set(destFunctionContext, newIndex, entry).FromMaybe(false)) {
                     Q_ASSERT(false);
@@ -1521,7 +1574,7 @@ void ScriptSignalV8Proxy::disconnect(ScriptValue arg0, ScriptValue arg1) {
                 newIndex++;
             }
         }
-        Q_ASSERT(foundIt);
+        Q_ASSERT(findCounter == 1);
         if (!destFunction->Set(destFunctionContext, destDataName, newArray).FromMaybe(false)) {
             Q_ASSERT(false);
         }
@@ -1532,8 +1585,30 @@ void ScriptSignalV8Proxy::disconnect(ScriptValue arg0, ScriptValue arg1) {
     // inform Qt that we're no longer connected to this signal
     if (_connections.empty()) {
         Q_ASSERT(_isConnected);
-        bool result = QMetaObject::disconnect(qobject, _meta.methodIndex(), this, _metaCallId);
-        Q_ASSERT(result);
+        // During cleanup qobject might be null since it might have been already deleted
+        if (!_cleanup || (_cleanup && qobject)) {
+            Q_ASSERT(qobject);
+            bool result = QMetaObject::disconnect(qobject, _meta.methodIndex(), this, _metaCallId);
+            Q_ASSERT(result);
+        }
         _isConnected = false;
     }
 }
+
+void ScriptSignalV8Proxy::disconnectAllScriptSignalProxies() {
+    _cleanup = true;
+    QList<Connection> connections;
+    withReadLock([&]{
+        connections = _connections;
+    });
+
+    for (auto &connection : connections) {
+        ScriptValue thisValue(new ScriptValueV8Wrapper(_engine, connection.thisValue));
+        ScriptValue callback(new ScriptValueV8Wrapper(_engine, connection.callback));
+        disconnect(thisValue, callback);
+    }
+}
+
+void ScriptSignalV8Proxy::disconnectAll() {
+    QObject::disconnect(this, nullptr, nullptr, nullptr);
+}
diff --git a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.h b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.h
index 8c7011daf1..9e3445ca98 100644
--- a/libraries/script-engine/src/v8/ScriptObjectV8Proxy.h
+++ b/libraries/script-engine/src/v8/ScriptObjectV8Proxy.h
@@ -133,6 +133,7 @@ private:  // storage
     QPointer<QObject> _object;
     // Handle for its own object
     v8::Persistent<v8::Object> _v8Object;
+    bool _wasDestroyed{false};
 
     Q_DISABLE_COPY(ScriptObjectV8Proxy)
 };
@@ -271,7 +272,10 @@ public:  // API
     QString fullName() const;
 
     // Disconnects all signals from the proxy
-    void disconnectAll() { QObject::disconnect(this, nullptr, nullptr, nullptr); };
+    void disconnectAll();
+
+    // This should be called only just before destruction of ScriptSignalV8Proxy
+    void disconnectAllScriptSignalProxies();
 
 private:  // storage
 
@@ -283,6 +287,9 @@ private:  // storage
     const int _metaCallId;
     ConnectionList _connections;
     bool _isConnected{ false };
+
+    // This allows skipping qobject check during disconnect, which is needed during cleanup because qobject is already deleted
+    bool _cleanup{ false };
     // Context in which it was created
     v8::UniquePersistent<v8::Context> _v8Context;
     // Call counter for debugging purposes. It can be used to determine which signals are overwhelming script engine.
diff --git a/libraries/script-engine/src/v8/ScriptValueV8Wrapper.cpp b/libraries/script-engine/src/v8/ScriptValueV8Wrapper.cpp
index fc329c9e19..7f9faf21f2 100644
--- a/libraries/script-engine/src/v8/ScriptValueV8Wrapper.cpp
+++ b/libraries/script-engine/src/v8/ScriptValueV8Wrapper.cpp
@@ -95,16 +95,22 @@ ScriptValue ScriptValueV8Wrapper::call(const ScriptValue& thisObject, const Scri
     if (v8This.get()->IsObject()) {
         recv = v8This.get();
     }else{
-        recv = _engine->getContext()->Global();
+        recv = context->Global();
     }
 
     lock.lockForRead();
-    auto maybeResult = v8Function->Call(_engine->getContext(), recv, args.length(), v8Args);
+    auto maybeResult = v8Function->Call(context, recv, args.length(), v8Args);
     lock.unlock();
     if (tryCatch.HasCaught()) {
         QString errorMessage(QString("Function call failed: \"") + _engine->formatErrorMessageFromTryCatch(tryCatch));
         if (_engine->_manager) {
-            _engine->_manager->scriptErrorMessage(errorMessage);
+            v8::Local<v8::Message> exceptionMessage = tryCatch.Message();
+            int errorLineNumber = -1;
+            if (!exceptionMessage.IsEmpty()) {
+                errorLineNumber = exceptionMessage->GetLineNumber(context).FromJust();
+            }
+            _engine->_manager->scriptErrorMessage(errorMessage, getFileNameFromTryCatch(tryCatch, isolate, context),
+                                                  errorLineNumber);
         } else {
             qDebug(scriptengine_v8) << errorMessage;
         }
@@ -114,9 +120,10 @@ ScriptValue ScriptValueV8Wrapper::call(const ScriptValue& thisObject, const Scri
     if (maybeResult.ToLocal(&result)) {
         return ScriptValue(new ScriptValueV8Wrapper(_engine, V8ScriptValue(_engine, result)));
     } else {
-        QString errorMessage("JS function call failed: " + _engine->currentContext()->backtrace().join("\n"));
+        auto currentContext = _engine->currentContext();
+        QString errorMessage("JS function call failed: " + currentContext->backtrace().join("\n"));
         if (_engine->_manager) {
-            _engine->_manager->scriptErrorMessage(errorMessage);
+            _engine->_manager->scriptErrorMessage(errorMessage, currentContext->currentFileName(), currentContext->currentLineNumber());
         } else {
             qDebug(scriptengine_v8) << errorMessage;
         }
@@ -156,7 +163,8 @@ ScriptValue ScriptValueV8Wrapper::construct(const ScriptValueList& args) {
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     Q_ASSERT(args.length() <= Q_METAMETHOD_INVOKE_MAX_ARGS);
     v8::Local<v8::Value> v8Args[Q_METAMETHOD_INVOKE_MAX_ARGS];
     int argIndex = 0;
@@ -174,7 +182,7 @@ ScriptValue ScriptValueV8Wrapper::construct(const ScriptValueList& args) {
     // V8TODO: I'm not sure if this is correct, maybe use CallAsConstructor instead?
     // Maybe it's CallAsConstructor for function and NewInstance for class?
     lock.lockForRead();
-    auto maybeResult = v8Function->NewInstance(_engine->getContext(), args.length(), v8Args);
+    auto maybeResult = v8Function->NewInstance(context, args.length(), v8Args);
     lock.unlock();
     v8::Local<v8::Object> result;
     if (maybeResult.ToLocal(&result)) {
@@ -207,13 +215,14 @@ ScriptValue ScriptValueV8Wrapper::data() const {
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     // Private properties are an experimental feature for now on V8, so we are using regular value for now
     if (_value.constGet()->IsObject()) {
         auto v8Object = v8::Local<v8::Object>::Cast(_value.constGet());
          v8::Local<v8::Value> data;
          //bool createData = false;
-         if (!v8Object->Get(_engine->getContext(), v8::String::NewFromUtf8(isolate, "__data").ToLocalChecked()).ToLocal(&data)) {
+         if (!v8Object->Get(context, v8::String::NewFromUtf8(isolate, "__data").ToLocalChecked()).ToLocal(&data)) {
              data = v8::Undefined(isolate);
              Q_ASSERT(false);
              //createData = true;
@@ -268,7 +277,8 @@ bool ScriptValueV8Wrapper::hasProperty(const QString& name) const {
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     //V8TODO: does function return true on IsObject too?
     if (_value.constGet()->IsObject()) {
     //V8TODO: what about flags?
@@ -276,7 +286,7 @@ bool ScriptValueV8Wrapper::hasProperty(const QString& name) const {
         v8::Local<v8::String> key = v8::String::NewFromUtf8(isolate, name.toStdString().c_str(),v8::NewStringType::kNormal).ToLocalChecked();
         const v8::Local<v8::Object> object = v8::Local<v8::Object>::Cast(_value.constGet());
         //V8TODO: Which context?
-        if (object->Get(_engine->getContext(), key).ToLocal(&resultLocal)) {
+        if (object->Get(context, key).ToLocal(&resultLocal)) {
             return true;
         } else {
             return false;
@@ -292,7 +302,8 @@ ScriptValue ScriptValueV8Wrapper::property(const QString& name, const ScriptValu
     v8::Locker locker(_engine->getIsolate());
     v8::Isolate::Scope isolateScope(_engine->getIsolate());
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     if (_value.constGet()->IsNullOrUndefined()) {
         return _engine->undefinedValue();
     }
@@ -303,14 +314,14 @@ ScriptValue ScriptValueV8Wrapper::property(const QString& name, const ScriptValu
         const v8::Local<v8::Object> object = v8::Local<v8::Object>::Cast(_value.constGet());
         //V8TODO: Which context?
         lock.lockForRead();
-        if (object->Get(_engine->getContext(), key).ToLocal(&resultLocal)) {
+        if (object->Get(context, key).ToLocal(&resultLocal)) {
             V8ScriptValue result(_engine, resultLocal);
             lock.unlock();
             return ScriptValue(new ScriptValueV8Wrapper(_engine, std::move(result)));
         } else {
             QString parentValueQString("");
             v8::Local<v8::String> parentValueString;
-            if (_value.constGet()->ToDetailString(_engine->getContext()).ToLocal(&parentValueString)) {
+            if (_value.constGet()->ToDetailString(context).ToLocal(&parentValueString)) {
                 QString(*v8::String::Utf8Value(isolate, parentValueString));
             }
             qCDebug(scriptengine_v8) << "Failed to get property, parent of value: " << name << ", parent type: " << QString(*v8::String::Utf8Value(isolate, _value.constGet()->TypeOf(isolate))) << " parent value: " << parentValueQString;
@@ -371,7 +382,8 @@ void ScriptValueV8Wrapper::setData(const ScriptValue& value) {
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     V8ScriptValue unwrapped = fullUnwrap(value);
     // Private properties are an experimental feature for now on V8, so we are using regular value for now
     if (_value.constGet()->IsNullOrUndefined()) {
@@ -380,7 +392,7 @@ void ScriptValueV8Wrapper::setData(const ScriptValue& value) {
     }
     if (_value.constGet()->IsObject()) {
         auto v8Object = v8::Local<v8::Object>::Cast(_value.constGet());
-        if( !v8Object->Set(_engine->getContext(), v8::String::NewFromUtf8(isolate, "__data").ToLocalChecked(), unwrapped.constGet()).FromMaybe(false)) {
+        if( !v8Object->Set(context, v8::String::NewFromUtf8(isolate, "__data").ToLocalChecked(), unwrapped.constGet()).FromMaybe(false)) {
             qCDebug(scriptengine_v8) << "ScriptValueV8Wrapper::data(): Data object couldn't be created";
             Q_ASSERT(false);
         }
@@ -396,7 +408,8 @@ void ScriptValueV8Wrapper::setProperty(const QString& name, const ScriptValue& v
     v8::Locker locker(_engine->getIsolate());
     v8::Isolate::Scope isolateScope(_engine->getIsolate());
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     V8ScriptValue unwrapped = fullUnwrap(value);
     if (_value.constGet()->IsNullOrUndefined()) {
         qCDebug(scriptengine_v8) << "ScriptValueV8Wrapper::setProperty() was called on a value that is null or undefined";
@@ -415,7 +428,7 @@ void ScriptValueV8Wrapper::setProperty(const QString& name, const ScriptValue& v
     } else {
         v8::Local<v8::String> details;
         QString detailsString("");
-        if(_value.get()->ToDetailString(_engine->getContext()).ToLocal(&details)) {
+        if(_value.get()->ToDetailString(context).ToLocal(&details)) {
             v8::String::Utf8Value utf8Value(isolate,details);
             detailsString = *utf8Value;
         }
@@ -431,7 +444,8 @@ void ScriptValueV8Wrapper::setProperty(quint32 arrayIndex, const ScriptValue& va
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     V8ScriptValue unwrapped = fullUnwrap(value);
     if (_value.constGet()->IsNullOrUndefined()) {
         qCDebug(scriptengine_v8) << "ScriptValueV8Wrapper::setProperty() was called on a value that is null or undefined";
@@ -441,7 +455,7 @@ void ScriptValueV8Wrapper::setProperty(quint32 arrayIndex, const ScriptValue& va
         auto object = v8::Local<v8::Object>::Cast(_value.get());
         //V8TODO: I don't know which context to use here
         lock.lockForRead();
-        v8::Maybe<bool> retVal(object->Set(_engine->getContext(), arrayIndex, unwrapped.constGet()));
+        v8::Maybe<bool> retVal(object->Set(context, arrayIndex, unwrapped.constGet()));
         lock.unlock();
         if (retVal.IsJust() ? !retVal.FromJust() : true){
             qCDebug(scriptengine_v8) << "Failed to set property";
@@ -531,9 +545,10 @@ qint32 ScriptValueV8Wrapper::toInt32() const {
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     v8::Local<v8::Integer> integer;
-    if (!_value.constGet()->ToInteger(_engine->getContext()).ToLocal(&integer)) {
+    if (!_value.constGet()->ToInteger(context).ToLocal(&integer)) {
         Q_ASSERT(false);
     }
     return static_cast<int32_t>((integer)->Value());
@@ -544,9 +559,10 @@ double ScriptValueV8Wrapper::toInteger() const {
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     v8::Local<v8::Integer> integer;
-    if (!_value.constGet()->ToInteger(_engine->getContext()).ToLocal(&integer)) {
+    if (!_value.constGet()->ToInteger(context).ToLocal(&integer)) {
         Q_ASSERT(false);
     }
     return (integer)->Value();
@@ -557,9 +573,10 @@ double ScriptValueV8Wrapper::toNumber() const {
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     v8::Local<v8::Number> number;
-    if (!_value.constGet()->ToNumber(_engine->getContext()).ToLocal(&number)) {
+    if (!_value.constGet()->ToNumber(context).ToLocal(&number)) {
         Q_ASSERT(false);
     }
     return number->Value();
@@ -581,9 +598,10 @@ quint16 ScriptValueV8Wrapper::toUInt16() const {
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     v8::Local<v8::Uint32> integer;
-    if (!_value.constGet()->ToUint32(_engine->getContext()).ToLocal(&integer)) {
+    if (!_value.constGet()->ToUint32(context).ToLocal(&integer)) {
         Q_ASSERT(false);
     }
     return static_cast<uint16_t>(integer->Value());
@@ -594,9 +612,10 @@ quint32 ScriptValueV8Wrapper::toUInt32() const {
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     v8::Local<v8::Uint32> integer;
-    if (!_value.constGet()->ToUint32(_engine->getContext()).ToLocal(&integer)) {
+    if (!_value.constGet()->ToUint32(context).ToLocal(&integer)) {
         Q_ASSERT(false);
     }
     return integer->Value();
@@ -632,16 +651,17 @@ bool ScriptValueV8Wrapper::equals(const ScriptValue& other) const {
     v8::Locker locker(isolate);
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
-    v8::Context::Scope contextScope(_engine->getContext());
+    auto context = _engine->getContext();
+    v8::Context::Scope contextScope(context);
     ScriptValueV8Wrapper* unwrappedOther = unwrap(other);
     Q_ASSERT(_engine->getIsolate() == unwrappedOther->_engine->getIsolate());
     if (!unwrappedOther) {
         return false;
     }else{
-        if (_value.constGet()->Equals(_engine->getContext(), unwrappedOther->toV8Value().constGet()).IsNothing()) {
+        if (_value.constGet()->Equals(context, unwrappedOther->toV8Value().constGet()).IsNothing()) {
             return false;
         } else {
-            return _value.constGet()->Equals(_engine->getContext(), unwrappedOther->toV8Value().constGet()).FromJust();
+            return _value.constGet()->Equals(context, unwrappedOther->toV8Value().constGet()).FromJust();
         }
     }
 }
@@ -670,7 +690,7 @@ bool ScriptValueV8Wrapper::isError() const {
     v8::Isolate::Scope isolateScope(isolate);
     v8::HandleScope handleScope(isolate);
     auto context = _engine->getContext();
-    v8::Context::Scope contextScope(_engine->getContext());
+    v8::Context::Scope contextScope(context);
     v8::Local<v8::Value> error;
     if (!context->Global()->Get(context, v8::String::NewFromUtf8(isolate, "Error").ToLocalChecked()).ToLocal(&error)) {
         Q_ASSERT(false);
diff --git a/libraries/shared/src/BlendshapeConstants.h b/libraries/shared/src/BlendshapeConstants.h
index 5dd1f0abbb..596e7df4ee 100644
--- a/libraries/shared/src/BlendshapeConstants.h
+++ b/libraries/shared/src/BlendshapeConstants.h
@@ -119,9 +119,9 @@ struct BlendshapeOffsetPacked {
 };
 
 struct BlendshapeOffsetUnpacked {
-    glm::vec3 positionOffset;
-    glm::vec3 normalOffset;
-    glm::vec3 tangentOffset;
+    float positionOffsetX, positionOffsetY, positionOffsetZ;
+    float normalOffsetX, normalOffsetY, normalOffsetZ;
+    float tangentOffsetX, tangentOffsetY, tangentOffsetZ;
 };
 
 using BlendshapeOffset = BlendshapeOffsetPacked;
diff --git a/libraries/shared/src/PortableHighResolutionClock.h b/libraries/shared/src/PortableHighResolutionClock.h
index 5650a27f3c..a13f23bf16 100644
--- a/libraries/shared/src/PortableHighResolutionClock.h
+++ b/libraries/shared/src/PortableHighResolutionClock.h
@@ -25,7 +25,9 @@
 
 #if defined(_MSC_VER) && _MSC_VER < 1900
 
+#ifndef WIN32_LEAN_AND_MEAN
 #define WIN32_LEAN_AND_MEAN
+#endif
 #include <windows.h>
 
 // The following struct is not compliant with the HF coding standard, but uses underscores to match the classes
diff --git a/libraries/shared/src/SettingHandle.cpp b/libraries/shared/src/SettingHandle.cpp
index 88785e5700..2353f30933 100644
--- a/libraries/shared/src/SettingHandle.cpp
+++ b/libraries/shared/src/SettingHandle.cpp
@@ -18,6 +18,8 @@
 
 Q_LOGGING_CATEGORY(settings_handle, "settings.handle")
 
+const QString SETTINGS_FULL_PRIVATE_GROUP_NAME = "fullPrivate";
+
 const QString Settings::firstRun { "firstRun" };
 
 
diff --git a/libraries/shared/src/SettingHandle.h b/libraries/shared/src/SettingHandle.h
index 2390063555..f8ba5f66ed 100644
--- a/libraries/shared/src/SettingHandle.h
+++ b/libraries/shared/src/SettingHandle.h
@@ -31,6 +31,15 @@
 
 Q_DECLARE_LOGGING_CATEGORY(settings_handle)
 
+/**
+ * @brief Name of the fully private settings group
+ *
+ * Settings in this group will be protected from reading and writing from script engines.
+ *
+ */
+
+extern const QString SETTINGS_FULL_PRIVATE_GROUP_NAME;
+
 /**
  * @brief QSettings analog
  *
diff --git a/libraries/shared/src/SimpleMovingAverage.h b/libraries/shared/src/SimpleMovingAverage.h
index 3855375f4c..53a71ba54f 100644
--- a/libraries/shared/src/SimpleMovingAverage.h
+++ b/libraries/shared/src/SimpleMovingAverage.h
@@ -48,8 +48,8 @@ private:
 
 template <class T, int MAX_NUM_SAMPLES> class MovingAverage {
 public:
-    MovingAverage<T, MAX_NUM_SAMPLES>() {}
-    MovingAverage<T, MAX_NUM_SAMPLES>(const MovingAverage<T, MAX_NUM_SAMPLES>& other) {
+    MovingAverage() {}
+    MovingAverage(const MovingAverage<T, MAX_NUM_SAMPLES>& other) {
         *this = other;
     }
     MovingAverage<T, MAX_NUM_SAMPLES>& operator=(const MovingAverage<T, MAX_NUM_SAMPLES>& other) {
diff --git a/libraries/shared/src/shared/WebRTC.h b/libraries/shared/src/shared/WebRTC.h
index 9f3c954b60..1ddccd8428 100644
--- a/libraries/shared/src/shared/WebRTC.h
+++ b/libraries/shared/src/shared/WebRTC.h
@@ -30,7 +30,9 @@
 #  define WEBRTC_DATA_CHANNELS 1
 #  define WEBRTC_WIN 1
 #  define NOMINMAX 1
+#ifndef WIN32_LEAN_AND_MEAN
 #  define WIN32_LEAN_AND_MEAN 1
+#endif
 #elif defined(Q_OS_ANDROID)
 // I don't yet have a working libwebrtc for android
 // #  define WEBRTC_AUDIO 1
diff --git a/libraries/ui/CMakeLists.txt b/libraries/ui/CMakeLists.txt
index 1af3ca0ba8..b9d41e3551 100644
--- a/libraries/ui/CMakeLists.txt
+++ b/libraries/ui/CMakeLists.txt
@@ -10,3 +10,7 @@ include_hifi_library_headers(controllers)
 # Required for some low level GL interaction in the OffscreenQMLSurface
 set(OpenGL_GL_PREFERENCE "GLVND")
 target_opengl()
+
+if (WIN32)
+  add_compile_definitions(_USE_MATH_DEFINES)
+endif()
\ No newline at end of file
diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp
index 474a8f467d..74734fdc43 100644
--- a/libraries/ui/src/QmlWindowClass.cpp
+++ b/libraries/ui/src/QmlWindowClass.cpp
@@ -279,6 +279,7 @@ bool QmlWindowClass::isVisible() {
         return quickItem->isVisible();
     } else {
         qDebug() << "QmlWindowClass::isVisible: asQuickItem() returned NULL";
+        return false;
     }
 }
 
diff --git a/libraries/ui/src/ui/OffscreenQmlSurface.cpp b/libraries/ui/src/ui/OffscreenQmlSurface.cpp
index b79a99b150..88c7329a4f 100644
--- a/libraries/ui/src/ui/OffscreenQmlSurface.cpp
+++ b/libraries/ui/src/ui/OffscreenQmlSurface.cpp
@@ -5,6 +5,7 @@
 //  Distributed under the Apache License, Version 2.0.
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
+
 #include "OffscreenQmlSurface.h"
 
 #include <unordered_set>
@@ -41,6 +42,7 @@
 #include <AbstractUriHandler.h>
 #include <AccountManager.h>
 #include <NetworkAccessManager.h>
+
 #include <GLMHelpers.h>
 #include <AudioClient.h>
 #include <shared/LocalFileAccessGate.h>
diff --git a/pkg-scripts/make-rpm-server b/pkg-scripts/make-rpm-server
index 146788329f..cf84bb97f2 100755
--- a/pkg-scripts/make-rpm-server
+++ b/pkg-scripts/make-rpm-server
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 # Copyright 2020-2021 Vircadia contributors.
-# Copyright 2022-2023 Overte e.V.
+# Copyright 2022-2024 Overte e.V.
 # SPDX-License-Identifier: Apache-2.0
 
 if [ "$OVERTE" = "" ]; then
@@ -55,7 +55,7 @@ DEPENDS=mesa-libGL,`ls \
 	| xargs -I {} sh -c 'objdump -p {} | grep NEEDED' \
 	| awk '{print $2}' \
 	| sort | uniq \
-	| egrep -v "^($SOFILES)$" \
+	| grep -E -v "^($SOFILES)$" \
 	| grep -v ^libGL \
 	| xargs -I {} sh -c "ldconfig -p | grep {} | tr ' ' '\n' | grep /" \
 	| xargs rpm -qf --queryformat "%{NAME}\n" \
@@ -73,7 +73,7 @@ DEPENDS=mesa-libGL,`ls \
 	| xargs -I {} sh -c 'objdump -p {} | grep NEEDED' \
 	| awk '{print $2}' \
 	| sort | uniq \
-	| egrep -v "^($SOFILES)$" \
+	| grep -E -v "^($SOFILES)$" \
 	| grep -v ^libGL \
 	| xargs -I {} sh -c "ldconfig -p | grep {} | tr ' ' '\n' | grep /" \
 	| xargs rpm -qf --queryformat "%{NAME}\n" \
diff --git a/pkg-scripts/overte-server.spec b/pkg-scripts/overte-server.spec
index 2857526295..cd542533b9 100644
--- a/pkg-scripts/overte-server.spec
+++ b/pkg-scripts/overte-server.spec
@@ -1,5 +1,5 @@
 # Copyright 2020-2021 Vircadia contributors.
-# Copyright 2022 Overte e.V.
+# Copyright 2022-2024 Overte e.V.
 # SPDX-License-Identifier: Apache-2.0
 
 #OVERTE=~/Overte rpmbuild --target x86_64 -bb overte-server.spec
@@ -75,11 +75,13 @@ chrpath -d $RPM_BUILD_ROOT/opt/overte/plugins/*.so
 chrpath -d $RPM_BUILD_ROOT/opt/overte/plugins/*/*.so
 strip --strip-all $RPM_BUILD_ROOT/opt/overte/plugins/*.so
 strip --strip-all $RPM_BUILD_ROOT/opt/overte/plugins/*/*.so
+install -d $RPM_BUILD_ROOT/usr/share/licenses/overte-server
+cp $OVERTE/LICENSE $RPM_BUILD_ROOT/usr/share/licenses/overte-server/LICENSE
 find $RPM_BUILD_ROOT/opt/overte/resources -name ".gitignore" -delete
 
 
 %files
-%license $OVERTE/LICENSE
+%license /usr/share/licenses/overte-server/LICENSE
 /opt/overte
 /usr/lib/systemd/system
 
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt
index 6f2bd56212..c68abefa77 100644
--- a/plugins/CMakeLists.txt
+++ b/plugins/CMakeLists.txt
@@ -1,9 +1,10 @@
 #
 #  Created by Bradley Austin Davis on 2015/10/25
 #  Copyright 2015 High Fidelity, Inc.
+#  Copyright 2023-2024 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
+#  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 #
 
 # add the plugin directories
@@ -12,13 +13,17 @@ list(REMOVE_ITEM PLUGIN_SUBDIRS "CMakeFiles")
 
 # client-side plugins
 if (NOT SERVER_ONLY AND NOT ANDROID)
+  if (WIN32 AND (NOT USE_GLES))
+      set(DIR "oculus")
+      add_subdirectory(${DIR})
+      set(DIR "oculusLegacy")
+      add_subdirectory(${DIR})
+  endif()
+
   if (NOT CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64")
-    set(DIR "oculus")
-    add_subdirectory(${DIR})
+    # Note: OpenVR is a Steam thing, which is different from OVR, which is an Oculus SDK component.
     set(DIR "openvr")
     add_subdirectory(${DIR})
-    set(DIR "oculusLegacy")
-    add_subdirectory(${DIR})
   endif()
 
   set(DIR "hifiSdl2")
@@ -31,8 +36,12 @@ if (NOT SERVER_ONLY AND NOT ANDROID)
 
   set(DIR "hifiSpacemouse")
   add_subdirectory(${DIR})
-  set(DIR "hifiNeuron")
-  add_subdirectory(${DIR})
+
+  if (USE_NEURON)
+    set(DIR "hifiNeuron")
+    add_subdirectory(${DIR})
+  endif()
+
   set(DIR "hifiKinect")
   add_subdirectory(${DIR})
 
diff --git a/plugins/JSAPIExample/src/JSAPIExample.cpp b/plugins/JSAPIExample/src/JSAPIExample.cpp
index ad23838e04..02b59ba412 100644
--- a/plugins/JSAPIExample/src/JSAPIExample.cpp
+++ b/plugins/JSAPIExample/src/JSAPIExample.cpp
@@ -165,7 +165,7 @@ namespace REPLACE_ME_WITH_UNIQUE_NAME {
           * settings = null; // optional best pratice; allows the object to be reclaimed ASAP by the JS garbage collector
           */
         ScriptValue getScopedSettings(const QString& scope) {
-            Q_ASSERT(engine);
+            Q_ASSERT(engine());
             auto engine = Scriptable::engine();
             if (!engine) {
                 return ScriptValue();
diff --git a/plugins/oculus/CMakeLists.txt b/plugins/oculus/CMakeLists.txt
index 941109c205..84167882ef 100644
--- a/plugins/oculus/CMakeLists.txt
+++ b/plugins/oculus/CMakeLists.txt
@@ -8,7 +8,7 @@
 #  SPDX-License-Identifier: Apache-2.0
 #
 
-if (WIN32 AND (NOT USE_GLES) AND (MSVC_VERSION LESS 1930) )
+if (WIN32 AND (NOT USE_GLES))
 
   # if we were passed an Oculus App ID for entitlement checks, send that along
   if (DEFINED ENV{OCULUS_APP_ID})
diff --git a/plugins/steamClient/src/SteamAPIPlugin.cpp b/plugins/steamClient/src/SteamAPIPlugin.cpp
index 853a53e881..b6e5b2ea77 100644
--- a/plugins/steamClient/src/SteamAPIPlugin.cpp
+++ b/plugins/steamClient/src/SteamAPIPlugin.cpp
@@ -73,7 +73,7 @@ HAuthTicket SteamTicketRequests::startRequest(TicketRequestCallback callback) {
     uint32 ticketSize { 0 };
     char ticket[MAX_TICKET_SIZE];
 
-    auto authTicket = SteamUser()->GetAuthSessionTicket(ticket, MAX_TICKET_SIZE, &ticketSize);
+    auto authTicket = SteamUser()->GetAuthSessionTicket(ticket, MAX_TICKET_SIZE, &ticketSize, NULL);
     qDebug() << "Got Steam auth session ticket:" << authTicket;
 
     if (authTicket == k_HAuthTicketInvalid) {
@@ -282,7 +282,8 @@ void SteamAPIPlugin::runCallbacks() {
         return;
     }
 
-    Steam_RunCallbacks(steamPipe, false);
+    //Steam_RunCallbacks(steamPipe, false);
+    SteamAPI_RunCallbacks();
 }
 
 void SteamAPIPlugin::requestTicket(TicketRequestCallback callback) {
diff --git a/prebuild.py b/prebuild.py
index 79a29cc198..4a567989b5 100644
--- a/prebuild.py
+++ b/prebuild.py
@@ -1,19 +1,19 @@
 #!python
 
-# The prebuild script is intended to simplify life for developers and dev-ops.  It's repsonsible for acquiring 
-# tools required by the build as well as dependencies on which we rely.  
-# 
+# The prebuild script is intended to simplify life for developers and dev-ops.  It's repsonsible for acquiring
+# tools required by the build as well as dependencies on which we rely.
+#
 # By using this script, we can reduce the requirements for a developer getting started to:
 #
 # * A working C++ dev environment like visual studio, xcode, gcc, or clang
-# * Qt 
+# * Qt
 # * CMake
 # * Python 3.x
 #
 # The function of the build script is to acquire, if not already present, all the other build requirements
-# The build script should be idempotent.  If you run it with the same arguments multiple times, that should 
-# have no negative impact on the subsequent build times (i.e. re-running the prebuild script should not 
-# trigger a header change that causes files to be rebuilt).  Subsequent runs after the first run should 
+# The build script should be idempotent.  If you run it with the same arguments multiple times, that should
+# have no negative impact on the subsequent build times (i.e. re-running the prebuild script should not
+# trigger a header change that causes files to be rebuilt).  Subsequent runs after the first run should
 # execute quickly, determining that no work is to be done
 
 import hifi_singleton
@@ -83,6 +83,10 @@ def parse_args():
     parser.add_argument('--build-root', required=True, type=str, help='The location of the cmake build')
     parser.add_argument('--ports-path', type=str, default=defaultPortsPath)
     parser.add_argument('--ci-build', action='store_true', default=os.getenv('CI_BUILD') is not None)
+    parser.add_argument('--get-vcpkg-id', action='store_true', help='Get the VCPKG ID, the hash path of the full VCPKG path')
+    parser.add_argument('--get-vcpkg-path', action='store_true', help='Get the full VCPKG path, ID included.')
+    parser.add_argument('--quiet', action='store_true', default=False, help='Quiet mode with less output')
+
     if True:
         args = parser.parse_args()
     else:
@@ -91,7 +95,7 @@ def parse_args():
 
 def main():
     # Fixup env variables.  Leaving `USE_CCACHE` on will cause scribe to fail to build
-    # VCPKG_ROOT seems to cause confusion on Windows systems that previously used it for 
+    # VCPKG_ROOT seems to cause confusion on Windows systems that previously used it for
     # building OpenSSL
     removeEnvVars = ['VCPKG_ROOT', 'USE_CCACHE']
     for var in removeEnvVars:
@@ -99,13 +103,31 @@ def main():
             del os.environ[var]
 
     args = parse_args()
-    assets_url = hifi_utils.readEnviromentVariableFromFile(args.build_root, 'EXTERNAL_BUILD_ASSETS')
+
+    if args.get_vcpkg_id or args.get_vcpkg_path:
+        # These arguments need quiet mode to avoid confusing scripts that use them.
+        args.quiet = True
+
+    if not args.quiet:
+        print(sys.argv)
 
     if args.ci_build:
         logging.basicConfig(datefmt='%H:%M:%S', format='%(asctime)s %(guid)s %(message)s', level=logging.INFO)
 
     logger.info('start')
 
+    pm = hifi_vcpkg.VcpkgRepo(args)
+
+    if args.get_vcpkg_id:
+        print(pm.id)
+        exit(0)
+
+    if args.get_vcpkg_path:
+        print(pm.path)
+        exit(0)
+
+    assets_url = hifi_utils.readEnviromentVariableFromFile(args.build_root, 'EXTERNAL_BUILD_ASSETS')
+
     # OS dependent information
     system = platform.system()
     if 'Windows' == system and 'CI_BUILD' in os.environ and os.environ["CI_BUILD"] == "Github":
@@ -129,11 +151,12 @@ def main():
                     qt.writeConfig()
         else:
             if (os.environ["OVERTE_USE_SYSTEM_QT"]):
-                print("System Qt selected")
+                if not args.quiet:
+                    print("System Qt selected")
+
             else:
                 raise Exception("Internal error: System Qt not selected, but hifi_qt.py failed to return a cmake path")
 
-    pm = hifi_vcpkg.VcpkgRepo(args)
     if qtInstallPath is not None:
         pm.writeVar('QT_CMAKE_PREFIX_PATH', qtInstallPath)
 
@@ -149,7 +172,7 @@ def main():
             if not pm.upToDate():
                 pm.bootstrap()
 
-        # Always write the tag, even if we changed nothing.  This 
+        # Always write the tag, even if we changed nothing.  This
         # allows vcpkg to reclaim disk space by identifying directories with
         # tags that haven't been touched in a long time
         pm.writeTag()
@@ -190,7 +213,7 @@ def main():
 
     logger.info('end')
 
-print(sys.argv)
+
 try:
     main()
 except hifi_utils.SilentFatalError as fatal_ex:
diff --git a/scripts/communityScripts/chat/FloofChat.html b/scripts/communityScripts/chat/FloofChat.html
index b2576ceba6..48be283f93 100644
--- a/scripts/communityScripts/chat/FloofChat.html
+++ b/scripts/communityScripts/chat/FloofChat.html
@@ -103,7 +103,7 @@
                 }
 
                 for (var i = 0; i < messageParts.length; i++) {
-                    messageFormatted.push(replaceFormatting(messageParts[i]));
+                    messageFormatted.push(messageParts[i]);
                 }
 
                 for (var i = 0; i < messageFormatted.length; i++) {
diff --git a/scripts/communityScripts/chat/FloofChat.js b/scripts/communityScripts/chat/FloofChat.js
index 84b18860b4..28abeed7b9 100644
--- a/scripts/communityScripts/chat/FloofChat.js
+++ b/scripts/communityScripts/chat/FloofChat.js
@@ -127,7 +127,7 @@ function connectWebSocket(timeout) {
             if (!muted["Grid"]) {
                 Messages.sendLocalMessage(FLOOF_NOTIFICATION_CHANNEL, JSON.stringify({
                     sender: "(G) " + cmd.displayName,
-                    text: replaceFormatting(cmd.message),
+                    text: cmd.message,
                     colour: {text: cmd.colour}
                 }));
             }
@@ -462,7 +462,7 @@ function messageReceived(channel, message, sender) {
                         if (!muted["Local"]) {
                             Messages.sendLocalMessage(FLOOF_NOTIFICATION_CHANNEL, JSON.stringify({
                                 sender: "(L) " + cmd.displayName,
-                                text: replaceFormatting(cmd.message),
+                                text: cmd.message,
                                 colour: {text: cmd.colour}
                             }));
                         }
@@ -477,7 +477,7 @@ function messageReceived(channel, message, sender) {
                     if (!muted["Domain"]) {
                         Messages.sendLocalMessage(FLOOF_NOTIFICATION_CHANNEL, JSON.stringify({
                             sender: "(D) " + cmd.displayName,
-                            text: replaceFormatting(cmd.message),
+                            text: cmd.message,
                             colour: {text: cmd.colour}
                         }));
                     }
@@ -491,7 +491,7 @@ function messageReceived(channel, message, sender) {
                     if (!muted["Grid"]) {
                         Messages.sendLocalMessage(FLOOF_NOTIFICATION_CHANNEL, JSON.stringify({
                             sender: "(G) " + cmd.displayName,
-                            text: replaceFormatting(cmd.message),
+                            text: cmd.message,
                             colour: {text: cmd.colour}
                         }));
                     }
@@ -504,7 +504,7 @@ function messageReceived(channel, message, sender) {
                     
                     Messages.sendLocalMessage(FLOOF_NOTIFICATION_CHANNEL, JSON.stringify({
                         sender: cmd.displayName,
-                        text: replaceFormatting(cmd.message),
+                        text: cmd.message,
                         colour: {text: cmd.colour}
                     }));
                 }
@@ -528,7 +528,7 @@ function messageReceived(channel, message, sender) {
                 
                 Messages.sendLocalMessage(FLOOF_NOTIFICATION_CHANNEL, JSON.stringify({
                     sender: "(" + cmd.category + ")",
-                    text: replaceFormatting(cmd.message),
+                    text: cmd.message,
                     colour: {text: cmd.colour}
                 }));
             }
diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js
index c3b432cea4..1cbadadb67 100644
--- a/scripts/defaultScripts.js
+++ b/scripts/defaultScripts.js
@@ -47,7 +47,7 @@ var DEFAULT_SCRIPTS_SEPARATE = [
     "communityScripts/notificationCore/notificationCore.js",
     "simplifiedUI/ui/simplifiedNametag/simplifiedNametag.js",
     {"stable": "system/more/app-more.js", "beta": "https://more.overte.org/more/app-more.js"},
-    {"stable": "communityScripts/chat/FloofChat.js", "beta": "https://content.fluffy.ws/scripts/chat/FloofChat.js"}
+    "communityScripts/chat/FloofChat.js",
     //"system/chat.js"
 ];
 
diff --git a/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/pickRayController.js b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/pickRayController.js
index 87d05fa838..fa9479b426 100644
--- a/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/pickRayController.js
+++ b/scripts/simplifiedUI/ui/simplifiedNametag/resources/modules/pickRayController.js
@@ -12,6 +12,9 @@
 
 
 var _this;
+
+var controllerStandard = Controller.Standard;
+
 function PickRayController(){
     _this = this;
 
@@ -36,9 +39,9 @@ function PickRayController(){
 
 // Returns the right UUID based on hand triggered
 function getUUIDFromLaser(hand) {
-    hand = hand === Controller.Standard.LeftHand
-        ? Controller.Standard.LeftHand
-        : Controller.Standard.RightHand;
+    hand = hand === controllerStandard.LeftHand
+        ? controllerStandard.LeftHand
+        : controllerStandard.RightHand;
 
     var pose = getControllerWorldLocation(hand);
     var start = pose.position;
@@ -61,7 +64,7 @@ function getGrabPointSphereOffset(handController) {
     // x = upward, y = forward, z = lateral
     var GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 };
     var offset = GRAB_POINT_SPHERE_OFFSET;
-    if (handController === Controller.Standard.LeftHand) {
+    if (handController === controllerStandard.LeftHand) {
         offset = {
             x: -GRAB_POINT_SPHERE_OFFSET.x,
             y: GRAB_POINT_SPHERE_OFFSET.y,
@@ -84,7 +87,7 @@ function getControllerWorldLocation(handController, doOffset) {
         valid = pose.valid;
         var controllerJointIndex;
         if (pose.valid) {
-            if (handController === Controller.Standard.RightHand) {
+            if (handController === controllerStandard.RightHand) {
                 controllerJointIndex = MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND");
             } else {
                 controllerJointIndex = MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_LEFTHAND");
@@ -192,21 +195,21 @@ function doublePressHandler(event) {
 function create(){
     _this.mapping = Controller.newMapping(_this.mappingName);
 
-    _this.mapping.from(Controller.Standard.LTClick).to(function (value) {
+    _this.mapping.from(controllerStandard.LTClick).to(function (value) {
         if (value === 0) {
             return;
         }
 
-        getUUIDFromLaser(Controller.Standard.LeftHand);
+        getUUIDFromLaser(controllerStandard.LeftHand);
     });
 
 
-    _this.mapping.from(Controller.Standard.RTClick).to(function (value) {
+    _this.mapping.from(controllerStandard.RTClick).to(function (value) {
         if (value === 0) {
             return;
         }
 
-        getUUIDFromLaser(Controller.Standard.RightHand);
+        getUUIDFromLaser(controllerStandard.RightHand);
     });
 
     return _this;
diff --git a/scripts/simplifiedUI/ui/simplifiedUI.js b/scripts/simplifiedUI/ui/simplifiedUI.js
index 2ce6a3e073..8023ce3318 100644
--- a/scripts/simplifiedUI/ui/simplifiedUI.js
+++ b/scripts/simplifiedUI/ui/simplifiedUI.js
@@ -7,6 +7,7 @@
 //  Authors: Wayne Chen & Zach Fox
 //  Created: 2019-05-01
 //  Copyright 2019 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -20,7 +21,7 @@ var DEFAULT_SCRIPTS_PATH_PREFIX = ScriptDiscoveryService.defaultScriptsPath + "/
 
 
 var MENU_NAMES = ["File", "Edit", "Display", "View", "Navigate", "Settings", "Developer", "Help"];
-var keepMenusSetting = Settings.getValue("simplifiedUI/keepMenus", false);
+var keepMenusSetting = Settings.getValue("simplifiedUI/keepMenus", true);
 function maybeRemoveDesktopMenu() {    
     if (!keepMenusSetting) {
         MENU_NAMES.forEach(function(menu) {
diff --git a/scripts/system/away.js b/scripts/system/away.js
index 87273b2727..3f41020c9e 100644
--- a/scripts/system/away.js
+++ b/scripts/system/away.js
@@ -17,6 +17,8 @@
 
 (function() { // BEGIN LOCAL_SCOPE
 
+var controllerStandard = Controller.Standard;
+
 var BASIC_TIMER_INTERVAL = 50; // 50ms = 20hz
 var OVERLAY_WIDTH = 1920;
 var OVERLAY_HEIGHT = 1080;
@@ -344,20 +346,20 @@ var maybeIntervalTimer = Script.setInterval(function() {
 
 Controller.mousePressEvent.connect(goActive);
 // Note peek() so as to not interfere with other mappings.
-eventMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(goActive);
-eventMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(goActive);
-eventMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(goActive);
-eventMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(goActive);
-eventMapping.from(Controller.Standard.LT).peek().to(goActive);
-eventMapping.from(Controller.Standard.LB).peek().to(goActive);
-eventMapping.from(Controller.Standard.LS).peek().to(goActive);
-eventMapping.from(Controller.Standard.LeftGrip).peek().to(goActive);
-eventMapping.from(Controller.Standard.RT).peek().to(goActive);
-eventMapping.from(Controller.Standard.RB).peek().to(goActive);
-eventMapping.from(Controller.Standard.RS).peek().to(goActive);
-eventMapping.from(Controller.Standard.RightGrip).peek().to(goActive);
-eventMapping.from(Controller.Standard.Back).peek().to(goActive);
-eventMapping.from(Controller.Standard.Start).peek().to(goActive);
+eventMapping.from(controllerStandard.LeftPrimaryThumb).peek().to(goActive);
+eventMapping.from(controllerStandard.RightPrimaryThumb).peek().to(goActive);
+eventMapping.from(controllerStandard.LeftSecondaryThumb).peek().to(goActive);
+eventMapping.from(controllerStandard.RightSecondaryThumb).peek().to(goActive);
+eventMapping.from(controllerStandard.LT).peek().to(goActive);
+eventMapping.from(controllerStandard.LB).peek().to(goActive);
+eventMapping.from(controllerStandard.LS).peek().to(goActive);
+eventMapping.from(controllerStandard.LeftGrip).peek().to(goActive);
+eventMapping.from(controllerStandard.RT).peek().to(goActive);
+eventMapping.from(controllerStandard.RB).peek().to(goActive);
+eventMapping.from(controllerStandard.RS).peek().to(goActive);
+eventMapping.from(controllerStandard.RightGrip).peek().to(goActive);
+eventMapping.from(controllerStandard.Back).peek().to(goActive);
+eventMapping.from(controllerStandard.Start).peek().to(goActive);
 Controller.enableMapping(eventMappingName);
 
 function awayStateWhenFocusLostInVRChanged(enabled) {
diff --git a/scripts/system/controllers/controllerDispatcher.js b/scripts/system/controllers/controllerDispatcher.js
index 16390a73bf..beee66221e 100644
--- a/scripts/system/controllers/controllerDispatcher.js
+++ b/scripts/system/controllers/controllerDispatcher.js
@@ -31,6 +31,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
 (function() {
     Script.include("/~/system/libraries/pointersUtils.js");
 
+    var controllerStandard = Controller.Standard;
+
     var NEAR_MAX_RADIUS = 0.1;
     var NEAR_TABLET_MAX_RADIUS = 0.05;
 
@@ -136,10 +138,10 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
 
         this.dataGatherers = {};
         this.dataGatherers.leftControllerLocation = function () {
-            return getControllerWorldLocation(Controller.Standard.LeftHand, true);
+            return getControllerWorldLocation(controllerStandard.LeftHand, true);
         };
         this.dataGatherers.rightControllerLocation = function () {
-            return getControllerWorldLocation(Controller.Standard.RightHand, true);
+            return getControllerWorldLocation(controllerStandard.RightHand, true);
         };
 
         this.updateTimings = function () {
@@ -178,8 +180,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
             var pinchOnBelowDistance = 0.016;
             var pinchOffAboveDistance = 0.035;
 
-            var leftIndexPose = Controller.getPoseValue(Controller.Standard.LeftHandIndex4);
-            var leftThumbPose = Controller.getPoseValue(Controller.Standard.LeftHandThumb4);
+            var leftIndexPose = Controller.getPoseValue(controllerStandard.LeftHandIndex4);
+            var leftThumbPose = Controller.getPoseValue(controllerStandard.LeftHandThumb4);
             var leftThumbToIndexDistance = Vec3.distance(leftIndexPose.translation, leftThumbPose.translation);
             if (leftIndexPose.valid && leftThumbPose.valid && leftThumbToIndexDistance < pinchOnBelowDistance) {
                 _this.leftTriggerClicked = 1;
@@ -191,8 +193,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
                 _this.leftTrackerClicked = false;
             }
 
-            var rightIndexPose = Controller.getPoseValue(Controller.Standard.RightHandIndex4);
-            var rightThumbPose = Controller.getPoseValue(Controller.Standard.RightHandThumb4);
+            var rightIndexPose = Controller.getPoseValue(controllerStandard.RightHandIndex4);
+            var rightThumbPose = Controller.getPoseValue(controllerStandard.RightHandThumb4);
             var rightThumbToIndexDistance = Vec3.distance(rightIndexPose.translation, rightThumbPose.translation);
             if (rightIndexPose.valid && rightThumbPose.valid && rightThumbToIndexDistance < pinchOnBelowDistance) {
                 _this.rightTriggerClicked = 1;
@@ -563,23 +565,23 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
 
         var MAPPING_NAME = "com.highfidelity.controllerDispatcher";
         var mapping = Controller.newMapping(MAPPING_NAME);
-        mapping.from([Controller.Standard.RT]).peek().to(_this.rightTriggerPress);
-        mapping.from([Controller.Standard.RTClick]).peek().to(_this.rightTriggerClick);
-        mapping.from([Controller.Standard.LT]).peek().to(_this.leftTriggerPress);
-        mapping.from([Controller.Standard.LTClick]).peek().to(_this.leftTriggerClick);
+        mapping.from([controllerStandard.RT]).peek().to(_this.rightTriggerPress);
+        mapping.from([controllerStandard.RTClick]).peek().to(_this.rightTriggerClick);
+        mapping.from([controllerStandard.LT]).peek().to(_this.leftTriggerPress);
+        mapping.from([controllerStandard.LTClick]).peek().to(_this.leftTriggerClick);
 
-        mapping.from([Controller.Standard.RB]).peek().to(_this.rightSecondaryPress);
-        mapping.from([Controller.Standard.LB]).peek().to(_this.leftSecondaryPress);
-        mapping.from([Controller.Standard.LeftGrip]).peek().to(_this.leftSecondaryPress);
-        mapping.from([Controller.Standard.RightGrip]).peek().to(_this.rightSecondaryPress);
+        mapping.from([controllerStandard.RB]).peek().to(_this.rightSecondaryPress);
+        mapping.from([controllerStandard.LB]).peek().to(_this.leftSecondaryPress);
+        mapping.from([controllerStandard.LeftGrip]).peek().to(_this.leftSecondaryPress);
+        mapping.from([controllerStandard.RightGrip]).peek().to(_this.rightSecondaryPress);
 
         Controller.enableMapping(MAPPING_NAME);
 
         this.leftPointer = this.pointerManager.createPointer(false, PickType.Ray, {
             joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND",
             filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE,
-            triggers: [{action: Controller.Standard.LTClick, button: "Focus"}, {action: Controller.Standard.LTClick, button: "Primary"}],
-            posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand, true),
+            triggers: [{action: controllerStandard.LTClick, button: "Focus"}, {action: controllerStandard.LTClick, button: "Primary"}],
+            posOffset: getGrabPointSphereOffset(controllerStandard.LeftHand, true),
             hover: true,
             scaleWithParent: true,
             distanceScaleEnd: true,
@@ -589,8 +591,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
         this.rightPointer = this.pointerManager.createPointer(false, PickType.Ray, {
             joint: "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND",
             filter: Picks.PICK_OVERLAYS | Picks.PICK_ENTITIES | Picks.PICK_INCLUDE_NONCOLLIDABLE,
-            triggers: [{action: Controller.Standard.RTClick, button: "Focus"}, {action: Controller.Standard.RTClick, button: "Primary"}],
-            posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand, true),
+            triggers: [{action: controllerStandard.RTClick, button: "Focus"}, {action: controllerStandard.RTClick, button: "Primary"}],
+            posOffset: getGrabPointSphereOffset(controllerStandard.RightHand, true),
             hover: true,
             scaleWithParent: true,
             distanceScaleEnd: true,
@@ -601,8 +603,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
             joint: "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND",
             filter: Picks.PICK_HUD,
             maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE,
-            posOffset: getGrabPointSphereOffset(Controller.Standard.LeftHand, true),
-            triggers: [{action: Controller.Standard.LTClick, button: "Focus"}, {action: Controller.Standard.LTClick, button: "Primary"}],
+            posOffset: getGrabPointSphereOffset(controllerStandard.LeftHand, true),
+            triggers: [{action: controllerStandard.LTClick, button: "Focus"}, {action: controllerStandard.LTClick, button: "Primary"}],
             hover: true,
             scaleWithParent: true,
             distanceScaleEnd: true,
@@ -612,8 +614,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
             joint: "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND",
             filter: Picks.PICK_HUD,
             maxDistance: DEFAULT_SEARCH_SPHERE_DISTANCE,
-            posOffset: getGrabPointSphereOffset(Controller.Standard.RightHand, true),
-            triggers: [{action: Controller.Standard.RTClick, button: "Focus"}, {action: Controller.Standard.RTClick, button: "Primary"}],
+            posOffset: getGrabPointSphereOffset(controllerStandard.RightHand, true),
+            triggers: [{action: controllerStandard.RTClick, button: "Focus"}, {action: controllerStandard.RTClick, button: "Primary"}],
             hover: true,
             scaleWithParent: true,
             distanceScaleEnd: true,
diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js
index 773d2852b7..4b24ef17b0 100644
--- a/scripts/system/controllers/controllerModules/equipEntity.js
+++ b/scripts/system/controllers/controllerModules/equipEntity.js
@@ -24,6 +24,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js");
 Script.include("/~/system/libraries/utils.js");
 
 
+var controllerStandard = Controller.Standard;
 var DEFAULT_SPHERE_MODEL_URL = Script.resolvePath("../../assets/models/equip-Fresnel-3.fbx");
 var EQUIP_SPHERE_SCALE_FACTOR = 0.65;
 
@@ -351,7 +352,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa
         };
 
         this.handToController = function() {
-            return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
+            return (this.hand === RIGHT_HAND) ? controllerStandard.RightHand : controllerStandard.LeftHand;
         };
 
         this.updateSmoothedTrigger = function(controllerData) {
diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js
index 540668748a..4c61e22764 100644
--- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js
@@ -22,6 +22,8 @@ Script.include("/~/system/libraries/controllers.js");
 
 (function() {
 
+    var controllerStandard = Controller.Standard;
+
     var MARGIN = 25;
 
     function TargetObject(entityID, entityProps) {
@@ -104,7 +106,7 @@ Script.include("/~/system/libraries/controllers.js");
 
 
         this.handToController = function() {
-            return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
+            return (this.hand === RIGHT_HAND) ? controllerStandard.RightHand : controllerStandard.LeftHand;
         };
 
         this.distanceGrabTimescale = function(mass, distance) {
@@ -188,7 +190,7 @@ Script.include("/~/system/libraries/controllers.js");
             var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix());
             var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition);
 
-            var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, DISPATCHER_PROPERTIES);
+            var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, "position");
             var now = Date.now();
             var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds
             this.currentObjectTime = now;
@@ -304,7 +306,7 @@ Script.include("/~/system/libraries/controllers.js");
 
         this.notPointingAtEntity = function(controllerData) {
             var intersection = controllerData.rayPicks[this.hand];
-            var entityProperty = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
+            var entityProperty = Entities.getEntityProperties(intersection.objectID, "type");
             var entityType = entityProperty.type;
             var hudRayPick = controllerData.hudRayPicks[this.hand];
             var point2d = this.calculateNewReticlePosition(hudRayPick.intersection);
@@ -339,7 +341,7 @@ Script.include("/~/system/libraries/controllers.js");
             var worldControllerPosition = controllerLocation.position;
             var worldControllerRotation = controllerLocation.orientation;
 
-            var grabbedProperties = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
+            var grabbedProperties = Entities.getEntityProperties(intersection.objectID, "position");
             this.currentObjectPosition = grabbedProperties.position;
             this.grabRadius = intersection.distance;
 
@@ -353,7 +355,7 @@ Script.include("/~/system/libraries/controllers.js");
         };
 
         this.targetIsNull = function() {
-            var properties = Entities.getEntityProperties(this.grabbedThingID, DISPATCHER_PROPERTIES);
+            var properties = Entities.getEntityProperties(this.grabbedThingID, "type");
             if (Object.keys(properties).length === 0 && this.distanceHolding) {
                 return true;
             }
diff --git a/scripts/system/controllers/controllerModules/farGrabEntity.js b/scripts/system/controllers/controllerModules/farGrabEntity.js
index 5849c60553..43109198fe 100644
--- a/scripts/system/controllers/controllerModules/farGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/farGrabEntity.js
@@ -19,6 +19,8 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js");
 Script.include("/~/system/libraries/controllers.js");
 
 (function () {
+    var controllerStandard = Controller.Standard;
+
     var MARGIN = 25;
 
     function TargetObject(entityID, entityProps) {
@@ -120,7 +122,7 @@ Script.include("/~/system/libraries/controllers.js");
         };
 
         this.handToController = function () {
-            return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
+            return (this.hand === RIGHT_HAND) ? controllerStandard.RightHand : controllerStandard.LeftHand;
         };
 
         this.distanceGrabTimescale = function (mass, distance) {
@@ -222,7 +224,7 @@ Script.include("/~/system/libraries/controllers.js");
             var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix());
             var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition);
 
-            var targetProps = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
+            var targetProps = Entities.getEntityProperties(this.targetEntityID, "position");
             var now = Date.now();
             var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds
             this.currentObjectTime = now;
@@ -276,7 +278,7 @@ Script.include("/~/system/libraries/controllers.js");
             // This block handles the user's ability to rotate the object they're FarGrabbing
             if (this.shouldManipulateTarget(controllerData)) {
                 // Get the pose of the controller that is not grabbing.
-                var pose = Controller.getPoseValue((this.getOffhand() ? Controller.Standard.RightHand : Controller.Standard.LeftHand));
+                var pose = Controller.getPoseValue((this.getOffhand() ? controllerStandard.RightHand : controllerStandard.LeftHand));
                 if (pose.valid) {
                     // If we weren't manipulating the object yet, initialize the entity's original position.
                     if (!this.manipulating) {
@@ -356,7 +358,7 @@ Script.include("/~/system/libraries/controllers.js");
 
         this.notPointingAtEntity = function (controllerData) {
             var intersection = controllerData.rayPicks[this.hand];
-            var entityProperty = Entities.getEntityProperties(intersection.objectID, DISPATCHER_PROPERTIES);
+            var entityProperty = Entities.getEntityProperties(intersection.objectID, "type");
             var entityType = entityProperty.type;
             var hudRayPick = controllerData.hudRayPicks[this.hand];
             var point2d = this.calculateNewReticlePosition(hudRayPick.intersection);
@@ -368,7 +370,7 @@ Script.include("/~/system/libraries/controllers.js");
         };
 
         this.targetIsNull = function () {
-            var properties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
+            var properties = Entities.getEntityProperties(this.targetEntityID, "type");
             if (Object.keys(properties).length === 0 && this.distanceHolding) {
                 return true;
             }
diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js
index f7d5b5a2dd..4c818647cd 100644
--- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js
+++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js
@@ -12,6 +12,7 @@
 
 /* global Script, Controller, RIGHT_HAND, LEFT_HAND, HMD, makeLaserParams */
 (function() {
+    var controllerStandard = Controller.Standard;
     Script.include("/~/system/libraries/controllers.js");
     var ControllerDispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js");
     var MARGIN = 25;
@@ -45,11 +46,11 @@
         };
 
         this.getOtherHandController = function() {
-            return (this.hand === RIGHT_HAND) ? Controller.Standard.LeftHand : Controller.Standard.RightHand;
+            return (this.hand === RIGHT_HAND) ? controllerStandard.LeftHand : controllerStandard.RightHand;
         };
 
         this.handToController = function() {
-            return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
+            return (this.hand === RIGHT_HAND) ? controllerStandard.RightHand : controllerStandard.LeftHand;
         };
 
         this.updateRecommendedArea = function() {
diff --git a/scripts/system/controllers/controllerModules/inEditMode.js b/scripts/system/controllers/controllerModules/inEditMode.js
index d553bf5714..22ce55c703 100644
--- a/scripts/system/controllers/controllerModules/inEditMode.js
+++ b/scripts/system/controllers/controllerModules/inEditMode.js
@@ -20,6 +20,7 @@ Script.include("/~/system/libraries/controllers.js");
 Script.include("/~/system/libraries/utils.js");
 
 (function () {
+    var controllerStandard = Controller.Standard;
     var MARGIN = 25;
     function InEditMode(hand) {
         this.hand = hand;
@@ -48,7 +49,7 @@ Script.include("/~/system/libraries/utils.js");
         };
 
         this.handToController = function() {
-            return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
+            return (this.hand === RIGHT_HAND) ? controllerStandard.RightHand : controllerStandard.LeftHand;
         };
 
         this.pointingAtTablet = function(objectID) {
@@ -70,7 +71,7 @@ Script.include("/~/system/libraries/utils.js");
 
         this.sendPickData = function(controllerData) {
             if (controllerData.triggerClicks[this.hand]) {
-                var hand = this.hand === RIGHT_HAND ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
+                var hand = this.hand === RIGHT_HAND ? controllerStandard.RightHand : controllerStandard.LeftHand;
                 if (!this.triggerClicked) {
                     print("inEditMode click");
                     this.selectedTarget = controllerData.rayPicks[this.hand];
diff --git a/scripts/system/controllers/controllerModules/nearGrabEntity.js b/scripts/system/controllers/controllerModules/nearGrabEntity.js
index 1a1b55c020..92fcc7ceb6 100644
--- a/scripts/system/controllers/controllerModules/nearGrabEntity.js
+++ b/scripts/system/controllers/controllerModules/nearGrabEntity.js
@@ -161,7 +161,7 @@ Script.include("/~/system/libraries/controllers.js");
 
                 var props = controllerData.nearbyEntityPropertiesByID[this.targetEntityID];
                 if (!props) {
-                    props = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
+                    props = Entities.getEntityProperties(this.targetEntityID, "type");
                     if (!props) {
                         // entity was deleted
                         this.grabbing = false;
diff --git a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js
index e0660df1b7..cb071c2dbd 100644
--- a/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js
+++ b/scripts/system/controllers/controllerModules/nearParentGrabOverlay.js
@@ -203,7 +203,7 @@ Script.include("/~/system/libraries/utils.js");
             var candidateOverlays = controllerData.nearbyOverlayIDs[this.hand];
             var grabbableOverlays = candidateOverlays.filter(function(overlayID) {
                 // V8TODO: check if this works
-                return Entities.getEntityProperties(overlayID, ["grab"]).grab.grabbable;
+                return Entities.getEntityProperties(overlayID, ["grab.grabbable"]).grab.grabbable;
             });
 
             var targetID = this.getTargetID(grabbableOverlays, controllerData);
diff --git a/scripts/system/controllers/controllerModules/teleport.js b/scripts/system/controllers/controllerModules/teleport.js
index 63106b6241..be45d3b70e 100644
--- a/scripts/system/controllers/controllerModules/teleport.js
+++ b/scripts/system/controllers/controllerModules/teleport.js
@@ -21,6 +21,8 @@ Script.include("/~/system/libraries/controllers.js");
 
 (function() { // BEGIN LOCAL_SCOPE
 
+    var controllerStandard = Controller.Standard;
+
     var TARGET_MODEL_URL = Script.resolvePath("../../assets/models/teleportationSpotBasev8.fbx");
     var SEAT_MODEL_URL = Script.resolvePath("../../assets/models/teleport-seat.fbx");
 
@@ -46,10 +48,10 @@ Script.include("/~/system/libraries/controllers.js");
 
     var handInfo = {
         right: {
-            controllerInput: Controller.Standard.RightHand
+            controllerInput: controllerStandard.RightHand
         },
         left: {
-            controllerInput: Controller.Standard.LeftHand
+            controllerInput: controllerStandard.LeftHand
         }
     };
 
@@ -1065,10 +1067,10 @@ Script.include("/~/system/libraries/controllers.js");
         registerGamePadMapping();
 
         // Teleport actions.
-        teleportMapping.from(Controller.Standard.LY).peek().to(leftTeleporter.getStandardLY);
-        teleportMapping.from(Controller.Standard.RY).peek().to(leftTeleporter.getStandardRY);
-        teleportMapping.from(Controller.Standard.LY).peek().to(rightTeleporter.getStandardLY);
-        teleportMapping.from(Controller.Standard.RY).peek().to(rightTeleporter.getStandardRY);
+        teleportMapping.from(controllerStandard.LY).peek().to(leftTeleporter.getStandardLY);
+        teleportMapping.from(controllerStandard.RY).peek().to(leftTeleporter.getStandardRY);
+        teleportMapping.from(controllerStandard.LY).peek().to(rightTeleporter.getStandardLY);
+        teleportMapping.from(controllerStandard.RY).peek().to(rightTeleporter.getStandardRY);
     }
 
     var leftTeleporter = new Teleporter(LEFT_HAND);
diff --git a/scripts/system/controllers/controllerModules/trackedHandTablet.js b/scripts/system/controllers/controllerModules/trackedHandTablet.js
index 66cf408af8..6aa5e88b34 100644
--- a/scripts/system/controllers/controllerModules/trackedHandTablet.js
+++ b/scripts/system/controllers/controllerModules/trackedHandTablet.js
@@ -12,6 +12,8 @@ Script.include("/~/system/libraries/controllers.js");
 
 (function() {
 
+    var controllerStandard = Controller.Standard;
+
     function TrackedHandTablet() {
         this.mappingName = 'hand-track-tablet-' + Math.random();
         this.inputMapping = Controller.newMapping(this.mappingName);
@@ -103,16 +105,16 @@ Script.include("/~/system/libraries/controllers.js");
 
         this.setup = function () {
             var _this = this;
-            this.inputMapping.from(Controller.Standard.LeftHandIndex4).peek().to(function (pose) {
+            this.inputMapping.from(controllerStandard.LeftHandIndex4).peek().to(function (pose) {
                 _this.leftIndexChanged(pose);
             });
-            this.inputMapping.from(Controller.Standard.LeftHandThumb4).peek().to(function (pose) {
+            this.inputMapping.from(controllerStandard.LeftHandThumb4).peek().to(function (pose) {
                 _this.leftThumbChanged(pose);
             });
-            this.inputMapping.from(Controller.Standard.RightHandIndex4).peek().to(function (pose) {
+            this.inputMapping.from(controllerStandard.RightHandIndex4).peek().to(function (pose) {
                 _this.rightIndexChanged(pose);
             });
-            this.inputMapping.from(Controller.Standard.RightHandThumb4).peek().to(function (pose) {
+            this.inputMapping.from(controllerStandard.RightHandThumb4).peek().to(function (pose) {
                 _this.rightThumbChanged(pose);
             });
 
diff --git a/scripts/system/controllers/controllerModules/trackedHandWalk.js b/scripts/system/controllers/controllerModules/trackedHandWalk.js
index 9ecc53a1fa..dc8f3f16c4 100644
--- a/scripts/system/controllers/controllerModules/trackedHandWalk.js
+++ b/scripts/system/controllers/controllerModules/trackedHandWalk.js
@@ -12,6 +12,8 @@ Script.include("/~/system/libraries/controllers.js");
 
 (function() {
 
+    var controllerStandard = Controller.Standard;
+
     function TrackedHandWalk() {
         this.gestureMappingName = 'hand-track-walk-gesture-' + Math.random();
         this.inputGestureMapping = Controller.newMapping(this.gestureMappingName);
@@ -114,16 +116,16 @@ Script.include("/~/system/libraries/controllers.js");
 
         this.setup = function () {
             var _this = this;
-            this.inputGestureMapping.from(Controller.Standard.LeftHandIndex4).peek().to(function (pose) {
+            this.inputGestureMapping.from(controllerStandard.LeftHandIndex4).peek().to(function (pose) {
                 _this.leftIndexChanged(pose);
             });
-            this.inputGestureMapping.from(Controller.Standard.LeftHandThumb4).peek().to(function (pose) {
+            this.inputGestureMapping.from(controllerStandard.LeftHandThumb4).peek().to(function (pose) {
                 _this.leftThumbChanged(pose);
             });
-            this.inputGestureMapping.from(Controller.Standard.RightHandIndex4).peek().to(function (pose) {
+            this.inputGestureMapping.from(controllerStandard.RightHandIndex4).peek().to(function (pose) {
                 _this.rightIndexChanged(pose);
             });
-            this.inputGestureMapping.from(Controller.Standard.RightHandThumb4).peek().to(function (pose) {
+            this.inputGestureMapping.from(controllerStandard.RightHandThumb4).peek().to(function (pose) {
                 _this.rightThumbChanged(pose);
             });
 
@@ -137,7 +139,7 @@ Script.include("/~/system/libraries/controllers.js");
                     // return currentPoint.z - _this.controlPoint.z;
                     return 0.5;
                 } else {
-                    // return Controller.getActionValue(Controller.Standard.TranslateZ);
+                    // return Controller.getActionValue(controllerStandard.TranslateZ);
                     return 0.0;
                 }
             }).to(Controller.Actions.TranslateZ);
@@ -147,7 +149,7 @@ Script.include("/~/system/libraries/controllers.js");
             //         var currentPoint = _this.getControlPoint();
             //         return currentPoint.x - _this.controlPoint.x;
             //     } else {
-            //         return Controller.getActionValue(Controller.Standard.Yaw);
+            //         return Controller.getActionValue(controllerStandard.Yaw);
             //     }
             // }).to(Controller.Actions.Yaw);
 
diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
index 77a01883b5..e5cccaf047 100644
--- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
+++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js
@@ -7,7 +7,7 @@
 
 /* global Script, Entities, enableDispatcherModule, disableDispatcherModule, makeRunningValues,
    makeDispatcherModuleParameters, Overlays, HMD, TRIGGER_ON_VALUE, TRIGGER_OFF_VALUE, getEnabledModuleByName,
-   Picks, makeLaserParams, Settings, MyAvatar, RIGHT_HAND, LEFT_HAND, DISPATCHER_PROPERTIES
+   Picks, makeLaserParams, Settings, MyAvatar, RIGHT_HAND, LEFT_HAND
 */
 
 Script.include("/~/system/libraries/controllerDispatcherUtils.js");
@@ -59,7 +59,7 @@ Script.include("/~/system/libraries/controllers.js");
                     var candidateOverlays = controllerData.nearbyOverlayIDs[this.hand];
                     var grabbableOverlays = candidateOverlays.filter(function(overlayID) {
                         //V8TODO: this needs to be checked if it works
-                        return Entities.getEntityProperties(overlayID, ["grab"]).grab.grabbable;
+                        return Entities.getEntityProperties(overlayID, ["grab.grabbable"]).grab.grabbable;
                     });
                     var target = nearGrabModule.getTargetID(grabbableOverlays, controllerData);
                     if (target) {
@@ -164,7 +164,7 @@ Script.include("/~/system/libraries/controllers.js");
                     return type;
                 }
             } else if (intersection.type === Picks.INTERSECTED_ENTITY) {
-                var entityProperties = Entities.getEntityProperties(objectID, DISPATCHER_PROPERTIES);
+                var entityProperties = Entities.getEntityProperties(objectID, ["type","locked"]);
                 var entityType = entityProperties.type;
                 var isLocked = entityProperties.locked;
                 if (entityType === "Web" && (!isLocked || triggerPressed)) {
diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js
index 459fad3425..a9e003be45 100644
--- a/scripts/system/controllers/grab.js
+++ b/scripts/system/controllers/grab.js
@@ -311,7 +311,7 @@ Grabber.prototype.pressEvent = function(event) {
     mouse.startDrag(event);
 
     var clickedEntity = pickResults.objectID;
-    var entityProperties = Entities.getEntityProperties(clickedEntity, DISPATCHER_PROPERTIES);
+    var entityProperties = Entities.getEntityProperties(clickedEntity, ["position", "rotation", "dimensions"]);
     this.startPosition = entityProperties.position;
     this.lastRotation = entityProperties.rotation;
     var cameraPosition = Camera.getPosition();
@@ -422,7 +422,7 @@ Grabber.prototype.moveEvent = function(event) {
 
 Grabber.prototype.moveEventProcess = function() {
     this.moveEventTimer = null;
-    var entityProperties = Entities.getEntityProperties(this.entityID, DISPATCHER_PROPERTIES);
+    var entityProperties = Entities.getEntityProperties(this.entityID, "position");
     if (!entityProperties || HMD.active) {
         return;
     }
diff --git a/scripts/system/controllers/handTouch.js b/scripts/system/controllers/handTouch.js
index 5939c6e3d2..6d5d403512 100644
--- a/scripts/system/controllers/handTouch.js
+++ b/scripts/system/controllers/handTouch.js
@@ -16,6 +16,7 @@
 
 (function () {
 
+    var controllerStandard = Controller.Standard;
     var LEAP_MOTION_NAME = "LeapMotion";
     // Hand touch is disabled due to twitchy finger bug when walking near walls or tables. see BUGZ-154.
     var handTouchEnabled = false;
@@ -792,15 +793,15 @@
 
     var MAPPING_NAME = "com.highfidelity.handTouch";
     var mapping = Controller.newMapping(MAPPING_NAME);
-    mapping.from([Controller.Standard.RT]).peek().to(rightTriggerPress);
-    mapping.from([Controller.Standard.RTClick]).peek().to(rightTriggerClick);
-    mapping.from([Controller.Standard.LT]).peek().to(leftTriggerPress);
-    mapping.from([Controller.Standard.LTClick]).peek().to(leftTriggerClick);
+    mapping.from([controllerStandard.RT]).peek().to(rightTriggerPress);
+    mapping.from([controllerStandard.RTClick]).peek().to(rightTriggerClick);
+    mapping.from([controllerStandard.LT]).peek().to(leftTriggerPress);
+    mapping.from([controllerStandard.LTClick]).peek().to(leftTriggerClick);
 
-    mapping.from([Controller.Standard.RB]).peek().to(rightSecondaryPress);
-    mapping.from([Controller.Standard.LB]).peek().to(leftSecondaryPress);
-    mapping.from([Controller.Standard.LeftGrip]).peek().to(leftSecondaryPress);
-    mapping.from([Controller.Standard.RightGrip]).peek().to(rightSecondaryPress);
+    mapping.from([controllerStandard.RB]).peek().to(rightSecondaryPress);
+    mapping.from([controllerStandard.LB]).peek().to(leftSecondaryPress);
+    mapping.from([controllerStandard.LeftGrip]).peek().to(leftSecondaryPress);
+    mapping.from([controllerStandard.RightGrip]).peek().to(rightSecondaryPress);
 
     Controller.enableMapping(MAPPING_NAME);
 
diff --git a/scripts/system/controllers/squeezeHands.js b/scripts/system/controllers/squeezeHands.js
index 69f44f46a9..3f6df92b12 100644
--- a/scripts/system/controllers/squeezeHands.js
+++ b/scripts/system/controllers/squeezeHands.js
@@ -16,6 +16,8 @@
 
 (function() { // BEGIN LOCAL_SCOPE
 
+var controllerStandard = Controller.Standard;
+
 var lastLeftTrigger = 0;
 var lastRightTrigger = 0;
 var leftHandOverlayAlpha = 0;
@@ -86,8 +88,8 @@ function animStateHandler(props) {
 }
 
 function update(dt) {
-    var leftTrigger = clamp(Controller.getValue(Controller.Standard.LT) + Controller.getValue(Controller.Standard.LeftGrip), 0, 1);
-    var rightTrigger = clamp(Controller.getValue(Controller.Standard.RT) + Controller.getValue(Controller.Standard.RightGrip), 0, 1);
+    var leftTrigger = clamp(Controller.getValue(controllerStandard.LT) + Controller.getValue(controllerStandard.LeftGrip), 0, 1);
+    var rightTrigger = clamp(Controller.getValue(controllerStandard.RT) + Controller.getValue(controllerStandard.RightGrip), 0, 1);
 
     //  Average last few trigger values together for a bit of smoothing
     var tau = clamp(dt / TRIGGER_SMOOTH_TIMESCALE, 0, 1);
@@ -95,7 +97,7 @@ function update(dt) {
     lastRightTrigger = lerp(rightTrigger, lastRightTrigger, tau);
 
     // ramp on/off left hand overlay
-    var leftHandPose = Controller.getPoseValue(Controller.Standard.LeftHand);
+    var leftHandPose = Controller.getPoseValue(controllerStandard.LeftHand);
     if (leftHandPose.valid) {
         leftHandOverlayAlpha = clamp(leftHandOverlayAlpha + OVERLAY_RAMP_RATE * dt, 0, 1);
     } else {
@@ -103,7 +105,7 @@ function update(dt) {
     }
 
     // ramp on/off right hand overlay
-    var rightHandPose = Controller.getPoseValue(Controller.Standard.RightHand);
+    var rightHandPose = Controller.getPoseValue(controllerStandard.RightHand);
     if (rightHandPose.valid) {
         rightHandOverlayAlpha = clamp(rightHandOverlayAlpha + OVERLAY_RAMP_RATE * dt, 0, 1);
     } else {
@@ -111,10 +113,10 @@ function update(dt) {
     }
 
     // Pointing index fingers and raising thumbs
-    isLeftIndexPointing = (leftIndexPointingOverride > 0) || (leftHandPose.valid && Controller.getValue(Controller.Standard.LeftIndexPoint) === 1);
-    isRightIndexPointing = (rightIndexPointingOverride > 0) || (rightHandPose.valid && Controller.getValue(Controller.Standard.RightIndexPoint) === 1);
-    isLeftThumbRaised = (leftThumbRaisedOverride > 0) || (leftHandPose.valid && Controller.getValue(Controller.Standard.LeftThumbUp) === 1);
-    isRightThumbRaised = (rightThumbRaisedOverride > 0) || (rightHandPose.valid && Controller.getValue(Controller.Standard.RightThumbUp) === 1);
+    isLeftIndexPointing = (leftIndexPointingOverride > 0) || (leftHandPose.valid && Controller.getValue(controllerStandard.LeftIndexPoint) === 1);
+    isRightIndexPointing = (rightIndexPointingOverride > 0) || (rightHandPose.valid && Controller.getValue(controllerStandard.RightIndexPoint) === 1);
+    isLeftThumbRaised = (leftThumbRaisedOverride > 0) || (leftHandPose.valid && Controller.getValue(controllerStandard.LeftThumbUp) === 1);
+    isRightThumbRaised = (rightThumbRaisedOverride > 0) || (rightHandPose.valid && Controller.getValue(controllerStandard.RightThumbUp) === 1);
 }
 
 function handleMessages(channel, message, sender) {
diff --git a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js
index 92f72f8724..b0b498fc29 100644
--- a/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js
+++ b/scripts/system/controllers/toggleAdvancedMovementForHandControllers.js
@@ -17,6 +17,8 @@
 
 (function() { // BEGIN LOCAL_SCOPE
 
+    var controllerStandard = Controller.Standard;
+
     var TWO_SECONDS_INTERVAL = 2000;
     var FLYING_MAPPING_NAME = 'Hifi-Flying-Dev-' + Math.random();
     var DRIVING_MAPPING_NAME = 'Hifi-Driving-Dev-' + Math.random();
@@ -44,7 +46,7 @@
     function registerBasicMapping() {
 
         drivingMapping = Controller.newMapping(DRIVING_MAPPING_NAME);
-        drivingMapping.from(Controller.Standard.LY).to(function(value) {
+        drivingMapping.from(controllerStandard.LY).to(function(value) {
             if (isDisabled) {
                 return;
             }
@@ -64,7 +66,7 @@
         });
 
         flyingMapping = Controller.newMapping(FLYING_MAPPING_NAME);
-        flyingMapping.from(Controller.Standard.RY).to(function(value) {
+        flyingMapping.from(controllerStandard.RY).to(function(value) {
             if (isDisabled) {
                 return;
             }
diff --git a/scripts/system/controllers/touchControllerConfiguration.js b/scripts/system/controllers/touchControllerConfiguration.js
index 991b77b8af..1944d54537 100644
--- a/scripts/system/controllers/touchControllerConfiguration.js
+++ b/scripts/system/controllers/touchControllerConfiguration.js
@@ -12,6 +12,8 @@
    Quat, Vec3, Script, MyAvatar, Controller */
 /* eslint camelcase: ["error", { "properties": "never" }] */
 
+var controllerStandard = Controller.Standard;
+
 var leftBaseRotation = Quat.multiply(
     Quat.fromPitchYawRollDegrees(-90, 0, 0),
     Quat.fromPitchYawRollDegrees(0, 0, 90)
@@ -89,7 +91,7 @@ TOUCH_CONTROLLER_CONFIGURATION_LEFT = {
                     naturalDimensions: { x: 0.027509, y: 0.025211, z: 0.018443 },
 
                     // rotational 
-                    input: Controller.Standard.LT,
+                    input: controllerStandard.LT,
                     origin: { x: 0, y: -0.015, z: -0.00 },
                     minValue: 0.0,
                     maxValue: 1.0,
diff --git a/scripts/system/controllers/viveControllerConfiguration.js b/scripts/system/controllers/viveControllerConfiguration.js
index 09fd8adacc..e0ae43c16c 100644
--- a/scripts/system/controllers/viveControllerConfiguration.js
+++ b/scripts/system/controllers/viveControllerConfiguration.js
@@ -15,6 +15,8 @@
 // var LEFT_JOINT_INDEX = MyAvatar.getJointIndex("_CONTROLLER_LEFTHAND");
 // var RIGHT_JOINT_INDEX = MyAvatar.getJointIndex("_CONTROLLER_RIGHTHAND");
 
+var controllerStandard = Controller.Standard;
+
 var leftBaseRotation = Quat.multiply(
     Quat.fromPitchYawRollDegrees(0, 0, 45),
     Quat.multiply(
@@ -141,7 +143,7 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = {
                 trigger: {
                     type: "rotational",
                     modelURL: BASE_URL + "meshes/controller/vive_trigger.fbx",
-                    input: Controller.Standard.LT,
+                    input: controllerStandard.LT,
                     naturalPosition: {"x":0.000004500150680541992,"y":-0.027690507471561432,"z":0.04830199480056763},
                     naturalDimensions: {x: 0.019105, y: 0.022189, z: 0.01909},
                     origin: { x: 0, y: -0.015, z: -0.00 },
@@ -283,7 +285,7 @@ VIVE_CONTROLLER_CONFIGURATION_RIGHT = {
                 trigger: {
                     type: "rotational",
                     modelURL: BASE_URL + "meshes/controller/vive_trigger.fbx",
-                    input: Controller.Standard.RT,
+                    input: controllerStandard.RT,
                     naturalPosition: {"x":0.000004500150680541992,"y":-0.027690507471561432,"z":0.04830199480056763},
                     naturalDimensions: {x: 0.019105, y: 0.022189, z: 0.01909},
                     origin: { x: 0, y: -0.015, z: -0.00 },
diff --git a/scripts/system/create/assets/data/createAppTooltips.json b/scripts/system/create/assets/data/createAppTooltips.json
index b67969b598..076f47f5dd 100644
--- a/scripts/system/create/assets/data/createAppTooltips.json
+++ b/scripts/system/create/assets/data/createAppTooltips.json
@@ -27,7 +27,7 @@
         "tooltip": "The height of each line of text. This determines the size of the text."
     },
     "font": {
-        "tooltip": "The font to render the text. Supported values: \"Courier\", \"Inconsolata\", \"Roboto\", \"Timeless\", or a URL to a .sdff file."
+        "tooltip": "The font to render the text. Supported values: \"Courier\", \"Inconsolata\", \"Roboto\", \"Timeless\", or a URL to a PNG MTSDF .arfont file."
     },
     "textEffect": {
         "tooltip": "The effect that is applied to the text."
diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js
index 1ddc2fa0d6..bfacd300e8 100644
--- a/scripts/system/create/edit.js
+++ b/scripts/system/create/edit.js
@@ -1,10 +1,10 @@
 //  edit.js
 //
-//  Created by Brad Hefta-Gaub on 10/2/14.
-//  Persist toolbar by HRS 6/11/15.
+//  Created by Brad Hefta-Gaub on October 2nd, 2014.
+//  Persist toolbar by HRS on June 2nd, 2015.
 //  Copyright 2014 High Fidelity, Inc.
 //  Copyright 2020 Vircadia contributors.
-//  Copyright 2022-2023 Overte e.V.
+//  Copyright 2022-2024 Overte e.V.
 //
 //  This script allows you to edit entities with a new UI/UX for mouse and trackpad based editing
 //
@@ -38,6 +38,7 @@
         "entitySelectionTool/entitySelectionTool.js",
         "audioFeedback/audioFeedback.js",
         "modules/brokenURLReport.js",
+        "modules/renderWithZonesManager.js",
         "editModes/editModes.js",
         "editModes/editVoxels.js"
     ]);
@@ -120,6 +121,18 @@
 
     var copiedPosition;
     var copiedRotation;
+    var copiedDimensions;
+
+    var importUiPersistedData = {
+        "elJsonUrl": "",
+        "elImportAtAvatar": true,
+        "elImportAtSpecificPosition": false,
+        "elPositionX": 0,
+        "elPositionY": 0,
+        "elPositionZ": 0,
+        "elEntityHostTypeDomain": true,
+        "elEntityHostTypeAvatar": false
+    };
 
     var cameraManager = new CameraManager();
 
@@ -2045,7 +2058,8 @@
         return position;
     }
 
-    function importSVO(importURL) {
+    function importSVO(importURL, importEntityHostType) {
+        importEntityHostType = importEntityHostType || "domain";
         if (!Entities.canRez() && !Entities.canRezTmp()) {
             Window.notifyEditError(INSUFFICIENT_PERMISSIONS_IMPORT_ERROR_MSG);
             return;
@@ -2068,7 +2082,7 @@
                 position = createApp.getPositionToCreateEntity(Clipboard.getClipboardContentsLargestDimension() / 2);
             }
             if (position !== null && position !== undefined) {
-                var pastedEntityIDs = Clipboard.pasteEntities(position);
+                var pastedEntityIDs = Clipboard.pasteEntities(position, importEntityHostType);
                 if (!isLargeImport) {
                     // The first entity in Clipboard gets the specified position with the rest being relative to it. Therefore, move
                     // entities after they're imported so that they're all the correct distance in front of and with geometric mean
@@ -2743,6 +2757,13 @@
                         copiedRotation = properties.rotation;
                         Window.copyToClipboard(JSON.stringify(copiedRotation));
                     }
+                } else if (data.action === "copyDimensions") {
+                    if (selectionManager.selections.length === 1) {
+                        selectionManager.saveProperties();
+                        properties = selectionManager.savedProperties[selectionManager.selections[0]];
+                        copiedDimensions = properties.dimensions;
+                        Window.copyToClipboard(JSON.stringify(copiedDimensions));
+                    }
                 } else if (data.action === "pastePosition") {
                     if (copiedPosition !== undefined && selectionManager.selections.length > 0 && SelectionManager.hasUnlockedSelection()) {
                         selectionManager.saveProperties();
@@ -2756,6 +2777,19 @@
                     } else {
                         audioFeedback.rejection();
                     }
+                } else if (data.action === "pasteDimensions") {
+                    if (copiedDimensions !== undefined && selectionManager.selections.length > 0 && SelectionManager.hasUnlockedSelection()) {
+                        selectionManager.saveProperties();
+                        for (i = 0; i < selectionManager.selections.length; i++) {
+                            Entities.editEntity(selectionManager.selections[i], {
+                                dimensions: copiedDimensions
+                            });
+                        }
+                        createApp.pushCommandForSelections();
+                        selectionManager._update(false, this);
+                    } else {
+                        audioFeedback.rejection();
+                    }
                 } else if (data.action === "pasteRotation") {
                     if (copiedRotation !== undefined  && selectionManager.selections.length > 0 && SelectionManager.hasUnlockedSelection()) {
                         selectionManager.saveProperties();
@@ -2789,6 +2823,10 @@
                     }
                 }
             } else if (data.type === "propertiesPageReady") {
+                emitScriptEvent({
+                    type: 'urlPermissionChanged',
+                    canViewAssetURLs: Entities.canViewAssetURLs(),
+                });
                 updateSelections(true);
             } else if (data.type === "tooltipsRequest") {
                 emitScriptEvent({
@@ -2828,6 +2866,72 @@
                     type: 'zoneListRequest',
                     zones: getExistingZoneList()
                 });
+            } else if (data.type === "importUiBrowse") {
+                let fileToImport = Window.browse("Select .json to Import", "", "*.json");
+                if (fileToImport !== null) {
+                     emitScriptEvent({
+                        type: 'importUi_SELECTED_FILE',
+                        file: fileToImport
+                    });
+                } else {
+                    audioFeedback.rejection();
+                }
+            } else if (data.type === "importUiImport") {
+                if ((data.entityHostType === "domain" && Entities.canAdjustLocks() && Entities.canRez()) || 
+                    (data.entityHostType === "avatar" && Entities.canRezAvatarEntities())) {
+                    if (data.positioningMode === "avatar") {
+                        importSVO(data.jsonURL, data.entityHostType);
+                    } else {
+                        if (Clipboard.importEntities(data.jsonURL)) {
+                            let importedPastedEntities = Clipboard.pasteEntities(data.position, data.entityHostType);
+                            if (importedPastedEntities.length === 0) {
+                                emitScriptEvent({
+                                    type: 'importUi_IMPORT_ERROR',
+                                    reason: "No Entity has been imported."
+                                });
+                            } else {
+                                if (isActive) {
+                                    selectionManager.setSelections(importedPastedEntities, this);
+                                }
+                                emitScriptEvent({type: 'importUi_IMPORT_CONFIRMATION'});
+                            }
+                        } else {
+                            emitScriptEvent({
+                                type: 'importUi_IMPORT_ERROR',
+                                reason: "Import Entities has failed."
+                            });
+                        }
+                    }
+                } else {
+                    emitScriptEvent({
+                        type: 'importUi_IMPORT_ERROR',
+                        reason: "You don't have permission to create in this domain."
+                    });
+                }
+            } else if (data.type === "importUiGoBack") {
+                if (location.canGoBack()) {
+                    location.goBack();
+                } else {
+                    audioFeedback.rejection();
+                }
+            } else if (data.type === "importUiGoTutorial") {
+                Window.location = "file:///~/serverless/tutorial.json";
+            } else if (data.type === "importUiGetCopiedPosition") {
+                if (copiedPosition !== undefined) {
+                    emitScriptEvent({
+                        type: 'importUi_POSITION_TO_PASTE',
+                        position: copiedPosition
+                    });
+                } else {
+                    audioFeedback.rejection();
+                }
+            } else if (data.type === "importUiPersistData") {
+                importUiPersistedData = data.importUiPersistedData;
+            } else if (data.type === "importUiGetPersistData") {
+                emitScriptEvent({
+                    type: 'importUi_LOAD_DATA',
+                    importUiPersistedData: importUiPersistedData
+                });
             }
         };
 
@@ -2838,6 +2942,13 @@
             });
         });
 
+        Entities.canViewAssetURLsChanged.connect((value) => {
+            emitScriptEvent({
+                type: 'urlPermissionChanged',
+                canViewAssetURLs: value,
+            });
+        });
+
         createToolsWindow.webEventReceived.addListener(this, onWebEventReceived);
 
         webView.webEventReceived.connect(this, onWebEventReceived);
diff --git a/scripts/system/create/editModes/editVoxels.js b/scripts/system/create/editModes/editVoxels.js
index 13138e55e1..bd0f4bc15a 100644
--- a/scripts/system/create/editModes/editVoxels.js
+++ b/scripts/system/create/editModes/editVoxels.js
@@ -34,6 +34,7 @@ EditVoxels = function() {
     var that = {};
 
     const NO_HAND = -1;
+    var controllerStandard = Controller.Standard;
 
     var controlHeld = false;
     var shiftHeld = false;
@@ -325,10 +326,10 @@ EditVoxels = function() {
             }
         }else{
             inverseOperation = false;
-            if(that.triggeredHand === Controller.Standard.RightHand && Controller.getValue(Controller.Standard.RightGrip) > 0.5){
+            if(that.triggeredHand === controllerStandard.RightHand && Controller.getValue(controllerStandard.RightGrip) > 0.5){
                 inverseOperation = true;
             }
-            if(that.triggeredHand === Controller.Standard.LeftHand && Controller.getValue(Controller.Standard.LeftGrip) > 0.5){
+            if(that.triggeredHand === controllerStandard.LeftHand && Controller.getValue(controllerStandard.LeftGrip) > 0.5){
                 inverseOperation = true;
             }
         }
@@ -458,13 +459,13 @@ EditVoxels = function() {
     }
 
     function getDistanceBetweenControllers(){
-        var poseLeft = getControllerWorldLocation(Controller.Standard.LeftHand, true);
-        var poseRight = getControllerWorldLocation(Controller.Standard.RightHand, true);
+        var poseLeft = getControllerWorldLocation(controllerStandard.LeftHand, true);
+        var poseRight = getControllerWorldLocation(controllerStandard.RightHand, true);
         return Vec3.distance(poseLeft.translation, poseRight.translation);
     }
     function getEditSpherePosition( radius ){
-        var poseLeft = getControllerWorldLocation(Controller.Standard.LeftHand, true);
-        var poseRight = getControllerWorldLocation(Controller.Standard.RightHand, true);
+        var poseLeft = getControllerWorldLocation(controllerStandard.LeftHand, true);
+        var poseRight = getControllerWorldLocation(controllerStandard.RightHand, true);
         var handsPosition = Vec3.multiply(Vec3.sum(poseLeft.translation, poseRight.translation), 0.5);
         return Vec3.sum(handsPosition, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0, z: radius * -2.0 }));
     }
@@ -531,15 +532,15 @@ EditVoxels = function() {
                 return;
             }
             if (value > 0.5) {
-                if (hand === Controller.Standard.LeftHand) {
+                if (hand === controllerStandard.LeftHand) {
                     isLeftGripPressed = true;
-                } else if (hand === Controller.Standard.RightHand) {
+                } else if (hand === controllerStandard.RightHand) {
                     isRightGripPressed = true;
                 }
             } else if (value < 0.4){
-                if (hand === Controller.Standard.LeftHand) {
+                if (hand === controllerStandard.LeftHand) {
                     isLeftGripPressed = false;
-                } else if (hand === Controller.Standard.RightHand) {
+                } else if (hand === controllerStandard.RightHand) {
                     isRightGripPressed = false;
                 }
             }
@@ -664,12 +665,12 @@ EditVoxels = function() {
     Controller.mouseReleaseEvent.connect(mouseReleaseEvent);
     Controller.keyPressEvent.connect(keyPressEvent);
     Controller.keyReleaseEvent.connect(keyReleaseEvent);
-    that.triggerClickMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand));
-    that.triggerClickMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand));
-    that.triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand));
-    that.triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand));
-    that.gripPressMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripPressHandler(Controller.Standard.LeftHand));
-    that.gripPressMapping.from(Controller.Standard.RightGrip).peek().to(makeGripPressHandler(Controller.Standard.RightHand));
+    that.triggerClickMapping.from(controllerStandard.RTClick).peek().to(makeClickHandler(controllerStandard.RightHand));
+    that.triggerClickMapping.from(controllerStandard.LTClick).peek().to(makeClickHandler(controllerStandard.LeftHand));
+    that.triggerPressMapping.from(controllerStandard.RT).peek().to(makePressHandler(controllerStandard.RightHand));
+    that.triggerPressMapping.from(controllerStandard.LT).peek().to(makePressHandler(controllerStandard.LeftHand));
+    that.gripPressMapping.from(controllerStandard.LeftGrip).peek().to(makeGripPressHandler(controllerStandard.LeftHand));
+    that.gripPressMapping.from(controllerStandard.RightGrip).peek().to(makeGripPressHandler(controllerStandard.RightHand));
     that.enableTriggerMapping = function() {
         that.triggerClickMapping.enable();
         that.triggerPressMapping.enable();
diff --git a/scripts/system/create/entityList/entityList.js b/scripts/system/create/entityList/entityList.js
index 257f967852..b8cb3bba33 100644
--- a/scripts/system/create/entityList/entityList.js
+++ b/scripts/system/create/entityList/entityList.js
@@ -4,7 +4,7 @@
 //
 //  Copyright 2014 High Fidelity, Inc.
 //  Copyright 2020 Vircadia contributors.
-//  Copyright 2023 Overte e.V.
+//  Copyright 2023-2024 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
@@ -351,6 +351,8 @@ var EntityListTool = function(shouldUseEditTabletApp, selectionManager) {
             that.selectionManager.cutSelectedEntities();
         } else if (data.type === "copy") {
             that.selectionManager.copySelectedEntities();
+        } else if (data.type === "copyID") {
+            that.selectionManager.copyIdsFromSelectedEntities();
         } else if (data.type === "paste") {
             that.selectionManager.pasteEntities();
         } else if (data.type === "duplicate") {
@@ -422,6 +424,8 @@ var EntityListTool = function(shouldUseEditTabletApp, selectionManager) {
             that.createApp.alignGridToAvatar();
         } else if (data.type === 'brokenURLReport') {
             brokenURLReport(that.selectionManager.selections);
+        } else if (data.type === 'renderWithZonesManager') {
+            renderWithZonesManager(that.selectionManager.selections);
         } else if (data.type === 'toggleGridVisibility') {
             that.createApp.toggleGridVisibility();
         } else if (data.type === 'toggleSnapToGrid') {
diff --git a/scripts/system/create/entityList/html/entityList.html b/scripts/system/create/entityList/html/entityList.html
index 75b172e201..2c024f256d 100644
--- a/scripts/system/create/entityList/html/entityList.html
+++ b/scripts/system/create/entityList/html/entityList.html
@@ -4,6 +4,7 @@
 //  Created by Ryan Huffman on 19 Nov 2014
 //  Copyright 2014 High Fidelity, Inc.
 //  Copyright 2020 Vircadia contributors.
+//  Copyright 2024 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
@@ -121,6 +122,12 @@
                     <div class = "menu-item-shortcut">Ctrl-C</div>
                 </div>
             </button>
+            <button class="menu-button" id="hmdcopyid" >
+                <div class = "menu-item">
+                    <div class = "menu-item-caption">Copy ID(s)</div>
+                    <div class = "menu-item-shortcut"></div>
+                </div>
+            </button>            
             <button class="menu-button" id="hmdpaste" >
                 <div class = "menu-item">
                     <div class = "menu-item-caption">Paste</div>
@@ -316,6 +323,12 @@
                     <div class = "menu-item-shortcut"></div>
                 </div>
             </button>
+            <button class="menu-button" id="renderWithZonesManager" >
+                <div class = "menu-item">
+                    <div class = "menu-item-caption">RenderWithZones Manager</div>
+                    <div class = "menu-item-shortcut"></div>
+                </div>
+            </button>
         </div>
         <div id="menuBackgroundOverlay" ></div>
     </body>
diff --git a/scripts/system/create/entityList/html/js/entityList.js b/scripts/system/create/entityList/html/js/entityList.js
index f83c334073..f544689e0b 100644
--- a/scripts/system/create/entityList/html/js/entityList.js
+++ b/scripts/system/create/entityList/html/js/entityList.js
@@ -1,8 +1,9 @@
 //  entityList.js
 //
-//  Created by Ryan Huffman on 19 Nov 2014
+//  Created by Ryan Huffman on November 19th, 2014
 //  Copyright 2014 High Fidelity, Inc.
 //  Copyright 2020 Vircadia contributors.
+//  Copyright 2024 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
@@ -274,6 +275,7 @@ let elEntityTable,
     elAlignGridToSelection,
     elAlignGridToAvatar,
     elBrokenURLReport,
+    elRenderWithZonesManager,
     elFilterTypeMultiselectBox,
     elFilterTypeText,
     elFilterTypeOptions,
@@ -327,6 +329,7 @@ function loaded() {
         elToolsMenu = document.getElementById("tools");
         elMenuBackgroundOverlay = document.getElementById("menuBackgroundOverlay");
         elHmdCopy = document.getElementById("hmdcopy");
+        elHmdCopyID = document.getElementById("hmdcopyid");
         elHmdCut = document.getElementById("hmdcut");
         elHmdPaste = document.getElementById("hmdpaste");
         elHmdDuplicate = document.getElementById("hmdduplicate");        
@@ -362,6 +365,7 @@ function loaded() {
         elAlignGridToSelection = document.getElementById("alignGridToSelection");
         elAlignGridToAvatar = document.getElementById("alignGridToAvatar");
         elBrokenURLReport = document.getElementById("brokenURLReport");
+        elRenderWithZonesManager = document.getElementById("renderWithZonesManager");
         elFilterTypeMultiselectBox = document.getElementById("filter-type-multiselect-box");
         elFilterTypeText = document.getElementById("filter-type-text");
         elFilterTypeOptions = document.getElementById("filter-type-options");
@@ -422,6 +426,10 @@ function loaded() {
             EventBridge.emitWebEvent(JSON.stringify({ type: "copy" }));
             closeAllEntityListMenu();
         };
+        elHmdCopyID.onclick = function() {
+            EventBridge.emitWebEvent(JSON.stringify({ type: "copyID" }));
+            closeAllEntityListMenu();
+        };
         elHmdCut.onclick = function() {
             EventBridge.emitWebEvent(JSON.stringify({ type: "cut" }));
             closeAllEntityListMenu();
@@ -604,6 +612,10 @@ function loaded() {
             EventBridge.emitWebEvent(JSON.stringify({ type: "brokenURLReport" }));
             closeAllEntityListMenu();
         };
+        elRenderWithZonesManager.onclick = function () {
+            EventBridge.emitWebEvent(JSON.stringify({ type: "renderWithZonesManager" }));
+            closeAllEntityListMenu();
+        };        
         elToggleSpaceMode.onclick = function() {
             EventBridge.emitWebEvent(JSON.stringify({ type: "toggleSpaceMode" }));
         };
@@ -816,6 +828,9 @@ function loaded() {
                 case "Copy":
                     EventBridge.emitWebEvent(JSON.stringify({ type: "copy" }));
                     break;
+                case "Copy ID(s)":
+                    EventBridge.emitWebEvent(JSON.stringify({ type: "copyID" }));
+                    break;
                 case "Paste":
                     EventBridge.emitWebEvent(JSON.stringify({ type: "paste" }));
                     break;
@@ -856,6 +871,10 @@ function loaded() {
                 enabledContextMenuItems.push("Rename");
                 enabledContextMenuItems.push("Delete");
             }
+            
+            if (selectedEntities.length !== 0) {
+                enabledContextMenuItems.push("Copy ID(s)");
+            }
 
             entityListContextMenu.open(clickEvent, entityID, enabledContextMenuItems);
         }
diff --git a/scripts/system/create/entityList/html/js/entityListContextMenu.js b/scripts/system/create/entityList/html/js/entityListContextMenu.js
index d71719f252..b1176a7dee 100644
--- a/scripts/system/create/entityList/html/js/entityListContextMenu.js
+++ b/scripts/system/create/entityList/html/js/entityListContextMenu.js
@@ -1,9 +1,10 @@
 //
 //  entityListContextMenu.js
 //
-//  exampleContextMenus.js was originally created by David Rowe on 22 Aug 2018.
-//  Modified to entityListContextMenu.js by Thijs Wenker on 10 Oct 2018
+//  exampleContextMenus.js was originally created by David Rowe on August 22nd, 2018.
+//  Modified to entityListContextMenu.js by Thijs Wenker on October 10th, 2018
 //  Copyright 2018 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -137,6 +138,7 @@ EntityListContextMenu.prototype = {
 
         this._addListItem("Cut");
         this._addListItem("Copy");
+        this._addListItem("Copy ID(s)");
         this._addListItem("Paste");
         this._addListSeparator();
         this._addListItem("Rename");
diff --git a/scripts/system/create/entityProperties/html/js/entityProperties.js b/scripts/system/create/entityProperties/html/js/entityProperties.js
index ff55abbde9..1eade2703d 100644
--- a/scripts/system/create/entityProperties/html/js/entityProperties.js
+++ b/scripts/system/create/entityProperties/html/js/entityProperties.js
@@ -1,9 +1,9 @@
 //  entityProperties.js
 //
-//  Created by Ryan Huffman on 13 Nov 2014
+//  Created by Ryan Huffman on November 13th, 2014
 //  Copyright 2014 High Fidelity, Inc.
 //  Copyright 2020 Vircadia contributors.
-//  Copyright 2022 Overte e.V.
+//  Copyright 2022-2024 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
@@ -152,7 +152,7 @@ const GROUPS = [
     },
     {
         id: "shape",
-        label: "SHAPE",        
+        label: "SHAPE",
         properties: [
             {
                 label: "Shape",
@@ -177,7 +177,7 @@ const GROUPS = [
                 decimals: 2,
                 propertyID: "shapeAlpha",
                 propertyName: "alpha",
-            },            
+            },
         ]
     },
     {
@@ -319,6 +319,7 @@ const GROUPS = [
                 type: "string",
                 propertyID: "zoneCompoundShapeURL",
                 propertyName: "compoundShapeURL", // actual entity property name
+                placeholder: "URL",
             },
             {
                 label: "Flying Allowed",
@@ -334,6 +335,7 @@ const GROUPS = [
                 label: "Filter",
                 type: "string",
                 propertyID: "filterURL",
+                placeholder: "URL",
             }
         ]
     },
@@ -409,7 +411,7 @@ const GROUPS = [
                 decimals: 2,
                 propertyID: "keyLight.shadowMaxDistance",
                 showPropertyRule: { "keyLightMode": "enabled" },
-            }    
+            }
         ]
     },    
     {
@@ -433,6 +435,7 @@ const GROUPS = [
                 type: "string",
                 propertyID: "skybox.url",
                 showPropertyRule: { "skyboxMode": "enabled" },
+                placeholder: "URL",
             }
         ]
     },
@@ -461,6 +464,7 @@ const GROUPS = [
                 type: "string",
                 propertyID: "ambientLight.ambientURL",
                 showPropertyRule: { "ambientLightMode": "enabled" },
+                placeholder: "URL",
             },
             {
                 type: "buttons",
@@ -678,6 +682,7 @@ const GROUPS = [
                 label: "Compound Shape",
                 type: "string",
                 propertyID: "compoundShapeURL",
+                placeholder: "URL",
             },
             {
                 label: "Use Original Pivot",
@@ -688,6 +693,7 @@ const GROUPS = [
                 label: "Animation",
                 type: "string",
                 propertyID: "animation.url",
+                placeholder: "URL",
             },
             {
                 label: "Play Automatically",
@@ -806,6 +812,7 @@ const GROUPS = [
                 label: "Source",
                 type: "string",
                 propertyID: "sourceUrl",
+                placeholder: "URL",
             },
             {
                 label: "Source Resolution",
@@ -932,6 +939,7 @@ const GROUPS = [
                 label: "Material URL",
                 type: "string",
                 propertyID: "materialURL",
+                placeholder: "URL",
             },
             {
                 label: "Material Data",
@@ -1100,6 +1108,7 @@ const GROUPS = [
                 type: "string",
                 propertyID: "particleCompoundShapeURL",
                 propertyName: "compoundShapeURL",
+                placeholder: "URL",
             },
             {
                 label: "Emit Dimensions",
@@ -1470,18 +1479,21 @@ const GROUPS = [
                 type: "string",
                 propertyID: "xTextureURL",
                 propertyName: "xTextureURL",
+                placeholder: "URL",
             },
             {
                 label: "Y Texture URL",
                 type: "string",
                 propertyID: "yTextureURL",
                 propertyName: "yTextureURL",
+                placeholder: "URL",
             },
             {
                 label: "Z Texture URL",
                 type: "string",
                 propertyID: "zTextureURL",
                 propertyName: "zTextureURL",
+                placeholder: "URL",
             },
         ]
     },
@@ -1568,6 +1580,12 @@ const GROUPS = [
                 propertyID: "localDimensions",
                 spaceMode: PROPERTY_SPACE_MODE.LOCAL,
             },
+            {
+                type: "buttons",
+                buttons: [  { id: "copyDimensions", label: "Copy Dimensions", className: "secondary", onClick: copyDimensionsProperty },
+                            { id: "pasteDimensions", label: "Paste Dimensions", className: "secondary", onClick: pasteDimensionsProperty } ],
+                propertyID: "copyPasteDimensions"
+            },
             {
                 label: "Scale",
                 type: "number-draggable",
@@ -1933,6 +1951,7 @@ let currentSelections = [];
 let createAppTooltip = new CreateAppTooltip();
 let currentSpaceMode = PROPERTY_SPACE_MODE.LOCAL;
 let zonesList = [];
+let canViewAssetURLs = false;
 
 function createElementFromHTML(htmlString) {
     let elTemplate = document.createElement('template');
@@ -2026,14 +2045,17 @@ function setCopyPastePositionAndRotationAvailability (selectionLength, islocked)
     if (selectionLength === 1) {
         $('#property-copyPastePosition-button-copyPosition').attr('disabled', false);
         $('#property-copyPasteRotation-button-copyRotation').attr('disabled', false);
+        $('#property-copyPasteDimensions-button-copyDimensions').attr('disabled', false);
     } else {
         $('#property-copyPastePosition-button-copyPosition').attr('disabled', true);
-        $('#property-copyPasteRotation-button-copyRotation').attr('disabled', true);        
+        $('#property-copyPasteRotation-button-copyRotation').attr('disabled', true);
+        $('#property-copyPasteDimensions-button-copyDimensions').attr('disabled', true);
     }
     
     if (selectionLength > 0 && !islocked) {
         $('#property-copyPastePosition-button-pastePosition').attr('disabled', false);
         $('#property-copyPasteRotation-button-pasteRotation').attr('disabled', false);
+        $('#property-copyPasteDimensions-button-pasteDimensions').attr('disabled', false);
         if (selectionLength === 1) {
             $('#property-copyPasteRotation-button-setRotationToZero').attr('disabled', false);
         } else {
@@ -2043,6 +2065,7 @@ function setCopyPastePositionAndRotationAvailability (selectionLength, islocked)
         $('#property-copyPastePosition-button-pastePosition').attr('disabled', true);
         $('#property-copyPasteRotation-button-pasteRotation').attr('disabled', true);
         $('#property-copyPasteRotation-button-setRotationToZero').attr('disabled', true);
+        $('#property-copyPasteDimensions-button-pasteDimensions').attr('disabled', true);
     }
 }
 
@@ -2598,7 +2621,7 @@ function createStringProperty(property, elProperty) {
     let elInput = createElementFromHTML(`
         <input id="${elementID}"
                type="text"
-               ${propertyData.placeholder ? 'placeholder="' + propertyData.placeholder + '"' : ''}
+               ${propertyData.placeholder ? 'placeholder="' + ((propertyData.placeholder === "URL" && !canViewAssetURLs) ? "You don't have permission to view this URL" : propertyData.placeholder) + '"' : ''}
                ${propertyData.readOnly ? 'readonly' : ''}/>
         `);
 
@@ -3479,27 +3502,41 @@ function pastePositionProperty() {
     EventBridge.emitWebEvent(JSON.stringify({
         type: "action",
         action: "pastePosition"
-    }));    
+    }));
 }
 
 function copyRotationProperty() {
     EventBridge.emitWebEvent(JSON.stringify({
         type: "action",
         action: "copyRotation"
-    }));    
+    }));
 }
 
 function pasteRotationProperty() {
     EventBridge.emitWebEvent(JSON.stringify({
         type: "action",
         action: "pasteRotation"
-    }));    
+    }));
 }
 function setRotationToZeroProperty() {
     EventBridge.emitWebEvent(JSON.stringify({
         type: "action",
         action: "setRotationToZero"
-    }));    
+    }));
+}
+
+function copyDimensionsProperty() {
+    EventBridge.emitWebEvent(JSON.stringify({
+        type: "action",
+        action: "copyDimensions"
+    }));
+}
+
+function pasteDimensionsProperty() {
+    EventBridge.emitWebEvent(JSON.stringify({
+        type: "action",
+        action: "pasteDimensions"
+    }));
 }
 /**
  * USER DATA FUNCTIONS
@@ -5218,7 +5255,7 @@ function loaded() {
                                     break;
                                 case 'vec3rgb':
                                     updateVectorMinMax(properties[property]);
-                                    break;                                    
+                                    break;
                                 case 'rect':
                                     updateRectMinMax(properties[property]);
                                     break;
@@ -5231,6 +5268,16 @@ function loaded() {
                     }
                 } else if (data.type === 'zoneListRequest') {
                     zonesList = data.zones;
+                } else if (data.type === 'urlPermissionChanged') {
+                    canViewAssetURLs = data.canViewAssetURLs;
+                    Object.entries(properties).forEach(function ([propertyID, property]) {
+                        if (property.data.placeholder && property.data.placeholder === "URL") {
+                            if (!canViewAssetURLs) {
+                                property.elInput.value = "";
+                            }
+                            property.elInput.placeholder = canViewAssetURLs ? property.data.placeholder : "You don't have permission to view this URL";
+                        }
+                    });
                 }
             });
 
diff --git a/scripts/system/create/entitySelectionTool/entitySelectionTool.js b/scripts/system/create/entitySelectionTool/entitySelectionTool.js
index a95dd94360..222ee6e11e 100644
--- a/scripts/system/create/entitySelectionTool/entitySelectionTool.js
+++ b/scripts/system/create/entitySelectionTool/entitySelectionTool.js
@@ -1,12 +1,12 @@
 //
 //  entitySelectionTool.js
 //
-//  Created by Brad hefta-Gaub on 10/1/14.
-//    Modified by Daniela Fontes * @DanielaFifo and Tiago Andrade @TagoWill on 4/7/2017
-//    Modified by David Back on 1/9/2018
+//  Created by Brad hefta-Gaub on October 1st, 2014.
+//    Modified by Daniela Fontes * @DanielaFifo and Tiago Andrade @TagoWill on April 7th, 2017
+//    Modified by David Back on January 9th, 2018
 //  Copyright 2014 High Fidelity, Inc.
 //  Copyright 2020 Vircadia contributors
-//  Copyright 2022-2023 Overte e.V.
+//  Copyright 2022-2024 Overte e.V.
 //
 //  This script implements a class useful for building tools for editing entities.
 //
@@ -23,6 +23,8 @@ const SPACE_WORLD = "world";
 const HIGHLIGHT_LIST_NAME = "editHandleHighlightList";
 const MIN_DISTANCE_TO_REZ_FROM_AVATAR = 3;
 
+var controllerStandard = Controller.Standard;
+
 Script.include([
     "../../libraries/controllers.js",
     "../../libraries/controllerDispatcherUtils.js",
@@ -149,7 +151,7 @@ SelectionManager = (function() {
                 that.clearSelections();
             }
         } else if (messageParsed.method === "pointingAt") {
-            if (messageParsed.hand === Controller.Standard.RightHand) {
+            if (messageParsed.hand === controllerStandard.RightHand) {
                 that.pointingAtDesktopWindowRight = messageParsed.desktopWindow;
                 that.pointingAtTabletRight = messageParsed.tablet;
             } else {
@@ -494,6 +496,18 @@ SelectionManager = (function() {
         that.createApp.deleteSelectedEntities();
     };
 
+    that.copyIdsFromSelectedEntities = function() {
+        if (that.selections.length === 0) {
+            audioFeedback.rejection();
+        } else if (that.selections.length === 1) {
+            Window.copyToClipboard(that.selections[0]);
+            audioFeedback.confirmation();
+        } else {
+            Window.copyToClipboard(JSON.stringify(that.selections));
+            audioFeedback.confirmation();
+        }
+    };
+
     that.copySelectedEntities = function() {
         var entityProperties = Entities.getMultipleEntityProperties(that.selections);
         var entityHostTypes = Entities.getMultipleEntityProperties(that.selections, 'entityHostType');
@@ -940,8 +954,8 @@ SelectionDisplay = (function() {
 
     var toolEntityNames = [];
     var lastControllerPoses = [
-        getControllerWorldLocation(Controller.Standard.LeftHand, true),
-        getControllerWorldLocation(Controller.Standard.RightHand, true)
+        getControllerWorldLocation(controllerStandard.LeftHand, true),
+        getControllerWorldLocation(controllerStandard.RightHand, true)
     ];
 
     var worldRotationX;
@@ -1323,12 +1337,12 @@ SelectionDisplay = (function() {
         return that.triggeredHand !== NO_HAND;
     };
     function pointingAtDesktopWindowOrTablet(hand) {
-        var pointingAtDesktopWindow = (hand === Controller.Standard.RightHand && 
+        var pointingAtDesktopWindow = (hand === controllerStandard.RightHand &&
                                        SelectionManager.pointingAtDesktopWindowRight) ||
-                                      (hand === Controller.Standard.LeftHand && 
+                                      (hand === controllerStandard.LeftHand &&
                                        SelectionManager.pointingAtDesktopWindowLeft);
-        var pointingAtTablet = (hand === Controller.Standard.RightHand && SelectionManager.pointingAtTabletRight) ||
-                               (hand === Controller.Standard.LeftHand && SelectionManager.pointingAtTabletLeft);
+        var pointingAtTablet = (hand === controllerStandard.RightHand && SelectionManager.pointingAtTabletRight) ||
+                               (hand === controllerStandard.LeftHand && SelectionManager.pointingAtTabletLeft);
         return pointingAtDesktopWindow || pointingAtTablet;
     }
     function makeClickHandler(hand) {
@@ -1363,10 +1377,10 @@ SelectionDisplay = (function() {
             }
         }
     }
-    that.triggerClickMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand));
-    that.triggerClickMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand));
-    that.triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand));
-    that.triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand));
+    that.triggerClickMapping.from(controllerStandard.RTClick).peek().to(makeClickHandler(controllerStandard.RightHand));
+    that.triggerClickMapping.from(controllerStandard.LTClick).peek().to(makeClickHandler(controllerStandard.LeftHand));
+    that.triggerPressMapping.from(controllerStandard.RT).peek().to(makePressHandler(controllerStandard.RightHand));
+    that.triggerPressMapping.from(controllerStandard.LT).peek().to(makePressHandler(controllerStandard.LeftHand));
     that.enableTriggerMapping = function() {
         that.triggerClickMapping.enable();
         that.triggerPressMapping.enable();
@@ -1494,7 +1508,7 @@ SelectionDisplay = (function() {
                     that.editingHand = that.triggeredHand;
                     Messages.sendLocalMessage(INEDIT_STATUS_CHANNEL, JSON.stringify({
                         method: "editing",
-                        hand: that.editingHand === Controller.Standard.LeftHand ? LEFT_HAND : RIGHT_HAND,
+                        hand: that.editingHand === controllerStandard.LeftHand ? LEFT_HAND : RIGHT_HAND,
                         editing: true
                     }));
                     activeTool.onBegin(event, pickRay, results);
@@ -1705,7 +1719,7 @@ SelectionDisplay = (function() {
                 }
                 Messages.sendLocalMessage(INEDIT_STATUS_CHANNEL, JSON.stringify({
                     method: "editing",
-                    hand: that.editingHand === Controller.Standard.LeftHand ? LEFT_HAND : RIGHT_HAND,
+                    hand: that.editingHand === controllerStandard.LeftHand ? LEFT_HAND : RIGHT_HAND,
                     editing: false
                 }));
                 that.editingHand = NO_HAND;
@@ -1775,7 +1789,7 @@ SelectionDisplay = (function() {
     that.checkControllerMove = function() {
         if (SelectionManager.hasSelection()) {
             var controllerPose = getControllerWorldLocation(that.triggeredHand, true);
-            var hand = (that.triggeredHand === Controller.Standard.LeftHand) ? 0 : 1;
+            var hand = (that.triggeredHand === controllerStandard.LeftHand) ? 0 : 1;
             if (controllerPose.valid && lastControllerPoses[hand].valid && that.triggered()) {
                 if (!Vec3.equal(controllerPose.position, lastControllerPoses[hand].position) ||
                     !Vec3.equal(controllerPose.rotation, lastControllerPoses[hand].rotation)) {
diff --git a/scripts/system/create/importEntities/html/css/importEntities.css b/scripts/system/create/importEntities/html/css/importEntities.css
new file mode 100644
index 0000000000..61c75dabb3
--- /dev/null
+++ b/scripts/system/create/importEntities/html/css/importEntities.css
@@ -0,0 +1,160 @@
+/*
+//  importEntities.css
+//
+//  Created by Alezia Kurdis on March 13th, 2024
+//  Copyright 2024 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
+*/
+
+@font-face {
+    font-family: FiraSans-SemiBold;
+    src: url(../../../../../../resources/fonts/FiraSans-SemiBold.ttf), /* Windows production */
+         url(../../../../../../fonts/FiraSans-SemiBold.ttf); /* OSX production */
+}
+
+@font-face {
+    font-family: FiraSans-Regular;
+    src: url(../../../../../../resources/fonts/FiraSans-Regular.ttf), /* Windows production */
+         url(../../../../../../fonts/FiraSans-Regular.ttf); /* OSX production */
+}
+
+@font-face {
+    font-family: Raleway-Bold;
+    src: url(../../../../../../resources/fonts/Raleway-Bold.ttf), /* Windows production */
+         url(../../../../../../fonts/Raleway-Bold.ttf); /* OSX production */
+}
+
+html {
+    width: 100%;
+    height: 100%;
+}
+input[type="text"] {
+    font-family: FiraSans-SemiBold;
+    color: #BBBBBB;
+    background-color: #222222;
+    border: 0;
+    padding: 4px;
+    margin: 1px;
+}
+
+input[type="number"] {
+    font-family: FiraSans-SemiBold;
+    color: #BBBBBB;
+    background-color: #222222;
+    border: 0;
+    padding: 4px;
+    margin: 1px;
+    width: 90px;
+}
+
+h2 {
+    font-size: 18px;
+    color: #FFFFFF;
+}
+body {
+    background: #404040;
+    font-family: FiraSans-Regular;
+    font-size: 14px;
+    color: #BBBBBB;
+    text-decoration: none;
+    font-style: normal;
+    font-variant: normal;
+    text-transform: none;
+}
+
+#importAtSpecificPositionContainer {
+    display: none;
+    width: 100%;
+}
+
+#jsonUrl {
+    width:90%;
+}
+#browseBtn {
+    font-family: FiraSans-SemiBold;
+}
+#browseBtn:hover {
+    
+}
+
+label {
+    font-family: FiraSans-SemiBold;
+    color: #DDDDDD;
+}
+font.red {
+    font-family: FiraSans-SemiBold;
+    color: #e83333;
+}
+font.green {
+    font-family: FiraSans-SemiBold;
+    color: #0db518;
+}
+font.blue {
+    font-family: FiraSans-SemiBold;
+    color: #447ef2;
+}
+#importBtn {
+    color: #ffffff;
+    background-color: #1080b8;
+    background: linear-gradient(#00b4ef 20%, #1080b8 100%);
+    font-family: Raleway-Bold;
+    font-size: 13px;
+    text-transform: uppercase;
+    vertical-align: top;
+    height: 28px;
+    min-width: 70px;
+    padding: 0 18px;
+    margin: 3px 3px 12px 3px;
+    border-radius: 5px;
+    border: 0;
+    cursor: pointer;
+}
+#importBtn:hover {
+    background: linear-gradient(#00b4ef, #00b4ef);
+    border: none;
+}
+input:focus {
+    outline: none;
+    color: #FFFFFF;
+}
+button:focus {
+    outline: none;
+}
+div.explicative {
+    width: 96%;
+    padding: 7px;
+    font-family: FiraSans-SemiBold;
+    font-size: 12px;
+    text-decoration: none;
+    color: #BBBBBB;
+}
+button.black {
+    font-family: Raleway-Bold;
+    font-size: 10px;
+    text-transform: uppercase;
+    vertical-align: top;
+    height: 18px;
+    min-width: 60px;
+    padding: 0 14px;
+    margin: 5px;
+    border-radius: 4px;
+    border: none;
+    color: #fff;
+    background-color: #000;
+    background: linear-gradient(#343434 20%, #000 100%);
+    cursor: pointer;
+}
+button.black:hover {
+    background: linear-gradient(#000, #000);
+    border: none;
+}
+#messageContainer {
+    font-family: FiraSans-SemiBold;
+    width: 100%;
+}
+#testContainer {
+    border: 1px solid #AAAAAA;
+    padding: 0px;
+}
diff --git a/scripts/system/create/importEntities/html/importEntities.html b/scripts/system/create/importEntities/html/importEntities.html
new file mode 100644
index 0000000000..a1550a642e
--- /dev/null
+++ b/scripts/system/create/importEntities/html/importEntities.html
@@ -0,0 +1,77 @@
+<!--
+//  importEntities.html
+//
+//  Created by Alezia Kurdis on March 13th, 2024.
+//  Copyright 2024 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
+-->
+<html>
+    <head>
+        <title>Import Entities</title>
+        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+        <link rel="stylesheet" type="text/css" href="css/importEntities.css">
+        <script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
+        <script type="text/javascript" src="js/importEntitiesUi.js"></script>
+    </head>
+    <body onload="loaded();" >
+        <h2>Import Entities (.json)</h2>
+        <font class="red">* </font>URL/File (.json):<br>
+        <input type="text" id = "jsonUrl">&nbsp;<button id="browseBtn">...</button><br>
+        <br>
+        <table style="width: 96%;">
+            <tr style="vertical-align: top;">
+                <td style="width: 40%;">
+                    Position:<br>
+                    &nbsp;&nbsp;&nbsp;<input type="radio" name="importAtPosition" id="importAtAvatar" value="avatar" checked><label for="importAtAvatar">&nbsp;In front of your avatar</label><br>
+                    &nbsp;&nbsp;&nbsp;<input type="radio" name="importAtPosition" id="importAtSpecificPosition" value="position"><label for="importAtSpecificPosition">&nbsp;At a specified Position</label><br>
+                </td>
+                <td style="width: 60%;">
+                    <div id="importAtSpecificPositionContainer">
+                        <font class="red">X</font> <input type="number" size="6" id = "positionX" value = "0">&nbsp;&nbsp;&nbsp;
+                        <font class="green">Y</font> <input type="number" size="6" id = "positionY" value = "0">&nbsp;&nbsp;&nbsp;
+                        <font class="blue">Z</font> <input type="number" size="6" id = "positionZ" value = "0"><br>
+                        <button id="pastePositionBtn" class="black">Paste Position</button><br>
+                        <div class="explicative">
+                            Note: If you import a "serverless" json file, such data include positions. 
+                            It this case, the "Position" will act as an offset.
+                        </div>
+                    </div>
+                </td>
+            </tr>
+        </table>
+        <br>
+        <table style="width: 96%;">
+            <tr style="vertical-align: top;">
+                <td style="width: 30%;">
+                    Entity Host Type:<br>
+                    &nbsp;&nbsp;&nbsp;<input type="radio" name="entityHostType" id="entityHostTypeDomain" value="domain" checked><label for="entityHostTypeDomain">&nbsp;Domain Entities</label><br>
+                    &nbsp;&nbsp;&nbsp;<input type="radio" name="entityHostType" id="entityHostTypeAvatar" value="avatar"><label for="entityHostTypeAvatar">&nbsp;Avatar Entities</label><br>
+                </td>
+                <td style="width: 70%;">
+                    <div id="messageContainer"></div>
+                </td>
+            </tr>
+        </table>
+        <div style="text-align: right; width:96%;"><button id="importBtn">IMPORT</button></div>
+        <div id="testContainer">
+            <table style="width: 96%;">
+                <tr style="vertical-align: top;">
+                    <td style="width: 60%;">
+                        <div class="explicative">
+                        For large import, it can be wise to test it in a serverless environment before doing it in your real domain.
+                        </div>
+                    </td>
+                    <td style="width: 40%;">
+                        <div style="text-align: center; width:96%;">
+                        <button id="backBtn" class="black">&#11164; Back</button>
+                        &nbsp;&nbsp;&nbsp;
+                        <button id="tpTutorialBtn" class="black">Go test &#11166;</button>
+                        </div>
+                    </td>
+                </tr>
+            </table>
+        </div>
+    </body>
+</html>
diff --git a/scripts/system/create/importEntities/html/js/importEntitiesUi.js b/scripts/system/create/importEntities/html/js/importEntitiesUi.js
new file mode 100644
index 0000000000..6e80c7f173
--- /dev/null
+++ b/scripts/system/create/importEntities/html/js/importEntitiesUi.js
@@ -0,0 +1,217 @@
+//  importEntitiesUi.js
+//
+//  Created by Alezia Kurdis on March 13th, 2024
+//  Copyright 2024 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
+
+let elJsonUrl;
+let elBrowseBtn;
+let elImportAtAvatar;
+let elImportAtSpecificPosition;
+let elImportAtSpecificPositionContainer;
+let elPositionX;
+let elPositionY;
+let elPositionZ;
+let elEntityHostTypeDomain;
+let elEntityHostTypeAvatar;
+let elMessageContainer;
+let elImportBtn;
+let elBackBtn;
+let elTpTutorialBtn;
+let elPastePositionBtn;
+
+let lockUntil;
+
+const LOCK_BTN_DELAY = 2000; //2 sec
+
+function loaded() {
+    lockUntil = 0;
+    
+    elJsonUrl = document.getElementById("jsonUrl");
+    elBrowseBtn = document.getElementById("browseBtn");
+    elImportAtAvatar = document.getElementById("importAtAvatar");
+    elImportAtSpecificPosition = document.getElementById("importAtSpecificPosition");
+    elImportAtSpecificPositionContainer = document.getElementById("importAtSpecificPositionContainer");
+    elPositionX = document.getElementById("positionX");
+    elPositionY = document.getElementById("positionY");
+    elPositionZ = document.getElementById("positionZ");
+    elEntityHostTypeDomain = document.getElementById("entityHostTypeDomain");
+    elEntityHostTypeAvatar = document.getElementById("entityHostTypeAvatar");
+    elMessageContainer = document.getElementById("messageContainer");
+    elImportBtn = document.getElementById("importBtn");
+    elBackBtn = document.getElementById("backBtn");
+    elTpTutorialBtn = document.getElementById("tpTutorialBtn");
+    elPastePositionBtn = document.getElementById("pastePositionBtn");
+    
+    elJsonUrl.oninput = function() {
+        persistData();
+    }
+    
+    elPositionX.oninput = function() {
+        persistData();
+    }
+    
+    elPositionY.oninput = function() {
+        persistData();
+    }
+    
+    elPositionZ.oninput = function() {
+        persistData();
+    }
+
+    elEntityHostTypeDomain.onclick = function() {
+        persistData();
+    }
+    
+    elEntityHostTypeAvatar.onclick = function() {
+        persistData();
+    }
+    
+    elBrowseBtn.onclick = function() {
+        const d = new Date();
+        let time = d.getTime();
+        if ((d.getTime() - lockUntil) > LOCK_BTN_DELAY) {
+            EventBridge.emitWebEvent(JSON.stringify({ "type": "importUiBrowse" }));
+            lockUntil = d.getTime() + LOCK_BTN_DELAY;
+        }
+    };
+    
+    elImportAtAvatar.onclick = function() {
+        elImportAtSpecificPositionContainer.style.display = "None";
+        persistData();
+    };
+
+    elImportAtSpecificPosition.onclick = function() {
+        elImportAtSpecificPositionContainer.style.display = "Block";
+        persistData();
+    };
+    
+    elImportBtn.onclick = function() {
+        const d = new Date();
+        let time = d.getTime();
+        if ((d.getTime() - lockUntil) > LOCK_BTN_DELAY) {
+            importJsonToWorld();
+            lockUntil = d.getTime() + LOCK_BTN_DELAY;
+        }
+    };
+    
+    elBackBtn.onclick = function() {
+        const d = new Date();
+        let time = d.getTime();
+        if ((d.getTime() - lockUntil) > LOCK_BTN_DELAY) {
+            EventBridge.emitWebEvent(JSON.stringify({ "type": "importUiGoBack" }));
+            lockUntil = d.getTime() + LOCK_BTN_DELAY;
+        }
+    };
+    
+    elTpTutorialBtn.onclick = function() {
+        const d = new Date();
+        let time = d.getTime();
+        if ((d.getTime() - lockUntil) > LOCK_BTN_DELAY) {
+            EventBridge.emitWebEvent(JSON.stringify({ "type": "importUiGoTutorial" }));
+            lockUntil = d.getTime() + LOCK_BTN_DELAY;
+        }
+    };
+    
+    elPastePositionBtn.onclick = function() {
+        const d = new Date();
+        let time = d.getTime();
+        if ((d.getTime() - lockUntil) > LOCK_BTN_DELAY) {
+            EventBridge.emitWebEvent(JSON.stringify({ "type": "importUiGetCopiedPosition" }));
+            lockUntil = d.getTime() + LOCK_BTN_DELAY;
+        }
+    };
+    
+    EventBridge.emitWebEvent(JSON.stringify({ "type": "importUiGetPersistData" }));
+}
+
+function persistData() {
+    let message = {
+        "type": "importUiPersistData",
+        "importUiPersistedData": {
+            "elJsonUrl": elJsonUrl.value,
+            "elImportAtAvatar": elImportAtAvatar.checked,
+            "elImportAtSpecificPosition": elImportAtSpecificPosition.checked,
+            "elPositionX": elPositionX.value,
+            "elPositionY": elPositionY.value,
+            "elPositionZ": elPositionZ.value,
+            "elEntityHostTypeDomain": elEntityHostTypeDomain.checked,
+            "elEntityHostTypeAvatar": elEntityHostTypeAvatar.checked
+        }
+    };
+    EventBridge.emitWebEvent(JSON.stringify(message));
+}
+
+function loadDataInUi(importUiPersistedData) {
+    elJsonUrl.value = importUiPersistedData.elJsonUrl;
+    elImportAtAvatar.checked = importUiPersistedData.elImportAtAvatar;
+    elImportAtSpecificPosition.checked = importUiPersistedData.elImportAtSpecificPosition;
+    elPositionX.value = importUiPersistedData.elPositionX;
+    elPositionY.value = importUiPersistedData.elPositionY;
+    elPositionZ.value = importUiPersistedData.elPositionZ;
+    elEntityHostTypeDomain.checked = importUiPersistedData.elEntityHostTypeDomain;
+    elEntityHostTypeAvatar.checked = importUiPersistedData.elEntityHostTypeAvatar;
+    if (elImportAtSpecificPosition.checked) {
+        elImportAtSpecificPositionContainer.style.display = "Block";
+    }
+}
+
+function importJsonToWorld() {
+    elMessageContainer.innerHTML = "";
+
+    if (elJsonUrl.value === "") {
+        elMessageContainer.innerHTML = "<div style = 'padding: 10px; color: #000000; background-color: #ff7700;'>ERROR: 'URL/File (.json)' is required.</div>";
+        return;
+    }
+    
+    let positioningMode = getRadioValue("importAtPosition");
+    let entityHostType = getRadioValue("entityHostType");
+
+    if (positioningMode === "position" && (elPositionX.value === "" || elPositionY.value === "" || elPositionZ.value === "")) {
+        elMessageContainer.innerHTML = "<div style = 'padding: 10px; color: #000000; background-color: #ff7700;'>ERROR: 'Position' is required.</div>";
+        return;
+    }
+    let position = {"x": parseFloat(elPositionX.value), "y": parseFloat(elPositionY.value), "z": parseFloat(elPositionZ.value)};
+    let message = {
+        "type": "importUiImport",
+        "jsonURL": elJsonUrl.value,
+        "positioningMode": positioningMode,
+        "position": position,
+        "entityHostType": entityHostType
+    };
+    EventBridge.emitWebEvent(JSON.stringify(message));
+}
+
+function getRadioValue(objectName) {
+    let radios = document.getElementsByName(objectName);
+    let i; 
+    let selectedValue = "";
+    for (i = 0; i < radios.length; i++) {
+        if (radios[i].checked) {
+            selectedValue = radios[i].value;
+            break;
+        }
+    }
+    return selectedValue;
+}
+
+EventBridge.scriptEventReceived.connect(function(message){
+    let messageObj = JSON.parse(message);
+    if (messageObj.type === "importUi_IMPORT_CONFIRMATION") {
+        elMessageContainer.innerHTML = "<div style = 'padding: 10px; color: #000000; background-color: #00ff00;'>IMPORT SUCCESSFUL.</div>";
+    } else if (messageObj.type === "importUi_IMPORT_ERROR") {
+        elMessageContainer.innerHTML = "<div style = 'padding: 10px; color: #FFFFFF; background-color: #ff0000;'>IMPORT ERROR: " + messageObj.reason + "</div>";
+    } else if (messageObj.type === "importUi_SELECTED_FILE") {
+        elJsonUrl.value = messageObj.file;
+        persistData();
+    } else if (messageObj.type === "importUi_POSITION_TO_PASTE") {
+        elPositionX.value = messageObj.position.x;
+        elPositionY.value = messageObj.position.y;
+        elPositionZ.value = messageObj.position.z;
+        persistData();
+    } else if (messageObj.type === "importUi_LOAD_DATA") {
+        loadDataInUi(messageObj.importUiPersistedData);
+    }
+});
diff --git a/scripts/system/create/modules/renderWithZonesManager.html b/scripts/system/create/modules/renderWithZonesManager.html
new file mode 100644
index 0000000000..03dd460159
--- /dev/null
+++ b/scripts/system/create/modules/renderWithZonesManager.html
@@ -0,0 +1,410 @@
+<!DOCTYPE html>
+<!--//
+//  renderWithZonesManager.html
+//
+//  Created by Alezia Kurdis on January 28th, 2024.
+//  Copyright 2024 Overte e.V.
+//
+//  Web Ui for renderWithZonesManager.js module.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//-->
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <title>RenderWithZones Manager</title>
+        <style>
+            @font-face {
+                font-family: Raleway-Regular;
+                src: url(../../../../resources/fonts/Raleway-Regular.ttf),  /* Windows production */
+                     url(../../../../fonts/Raleway-Regular.ttf),  /* OSX production */
+                     url(../../../../interface/resources/fonts/Raleway-Regular.ttf),  /* Development, running script in /HiFi/examples */
+                     url(../fonts/Raleway-Regular.ttf);  /* Marketplace script */
+            }
+
+            @font-face {
+                font-family: Raleway-Bold;
+                src: url(../../../../resources/fonts/Raleway-Bold.ttf),
+                     url(../../../../fonts/Raleway-Bold.ttf),
+                     url(../../../../interface/resources/fonts/Raleway-Bold.ttf),
+                     url(../fonts/Raleway-Bold.ttf);
+            }
+            
+            @font-face {
+                font-family: HiFi-Glyphs;
+                src: url(../../../../resources/fonts/hifi-glyphs.ttf),
+                     url(../../../../fonts/hifi-glyphs.ttf),
+                     url(../../../../interface/resources/fonts/hifi-glyphs.ttf),
+                     url(../fonts/hifi-glyphs.ttf);
+            }
+            
+            body {
+                background-color:#000000;
+                color:#ffffff;
+                font-family: Raleway-Bold;
+                font-size: 11px;
+                width: 96%;
+                padding: 0;
+                margin: 0;
+            }
+            #rwzmUI {
+                width: 100%;
+                padding: 6px;
+            }
+            h1 {
+                font-family: Raleway-Bold;
+                font-size: 22px;
+            }
+            h2 {
+                font-family: Raleway-Bold;
+                font-size: 16px;
+            }            
+            table {
+                width: 98%;
+                border-collapse: collapse;
+            }
+            
+            td.cells {
+                background-color: #222222;
+                border: 1px solid #FFFFFF;
+                vertical-align: top;
+                height: 23px;
+            }
+            td.highlightedCells {
+                background-color: #3bc7ff;
+                border: 1px solid #FFFFFF;
+                vertical-align: top;
+                height: 23px;
+            }
+            td.header {
+                background-color: #444444;
+                color: #DDDDDD;
+            }
+            
+            td.line {
+                color: #FFFFFF;
+            }
+            td.errorline {
+                color: #ff5900;
+            }
+            td.lineInverted {
+                color: #000000;
+            }
+            div.warning {
+                color: #ff5900;
+                padding: 2px;
+                margin: 3px;
+                font-family: Raleway-Regular;
+                font-size: 12px;
+            }
+            a {
+                font-size: 22px;
+                font-weight: 500;
+            }
+            a:link {
+                color: #00b3ff;
+                background-color: transparent;
+                text-decoration: none;
+            }
+
+            a:visited {
+                color: #00b3ff;
+                background-color: transparent;
+                text-decoration: none;
+            }
+
+            a:hover {
+                color: #99e1ff;
+                background-color: transparent;
+                text-decoration: none;
+            }
+
+            a:active {
+                color: #99e1ff;
+                background-color: transparent;
+                text-decoration: none;
+            }
+            font.hifiGlyphs {
+                font-family: HiFi-Glyphs;
+                font-size: 12px;
+            }
+            span.delBtn {
+                color: #ad7171; font-size: 16px;
+            }
+            span.delBtn:hover {
+                color: #d65151;
+            }
+            button.addbtn {
+                margin: 2px;
+                border-radius: 4px;
+                border: 0px;
+                color: #ffffff;
+                font-family: Raleway-Bold;
+                font-size: 10px;
+                background-color: #57ad4f;
+                padding: 2px 6px 2px 6px;
+                text-decoration: none;
+            }
+            button.addbtn:hover {
+                background-color: #4fe63e;
+                text-decoration: none;
+            }
+            button:focus {
+                outline: none;
+            }
+            #rwzmAddZoneSelector, #rwzmReplaceZoneSelector {
+                position: absolute;
+                display: none;
+                width: 96%;
+                height: 100%;
+                top: 0px;
+                left: 0px;
+                right: 0px;
+                bottom: 0px;
+                border-width: 0px;
+                background-color: #666666;
+                color: #ffffff;
+                padding: 0% 2% 0% 2%;
+                z-index: 2;
+                cursor: pointer;
+            }
+            div.zoneSelectorContainer {
+                marging: 3%;
+                width: 100%;
+                height: 500px;
+                background-color: #c0c0c0;
+                overflow-y: auto;
+                padding: 0px;
+            }
+            button.greyBtn {
+                margin: 4px;
+                border-radius: 4px;
+                border: 0px;
+                color: #dddddd;
+                font-family: Raleway-Bold;
+                font-size: 14px;
+                background-color: #404040;
+                padding: 4px 8px 4px 8px;
+                text-decoration: none;
+            }
+            button.greyBtn:hover {
+                background-color: #828282;
+                color: #ffffff;
+                text-decoration: none;
+            }
+            button.small {
+                margin: 2px;
+                font-size: 10px;
+                padding: 2px 6px 2px 6px;
+            }
+            button.redBtn {
+                margin: 2px;
+                border-radius: 4px;
+                border: 0px;
+                color: #dddddd;
+                font-family: Raleway-Bold;
+                font-size: 10px;
+                background-color: #ba3d3d;
+                padding: 2px 6px 2px 6px;
+                text-decoration: none;
+            }
+            button.redBtn:hover {
+                background-color: #d60202;
+                color: #ffffff;
+                text-decoration: none;
+            }
+            button.blueBtn {
+                margin: 2px;
+                border-radius: 4px;
+                border: 0px;
+                color: #dddddd;
+                font-family: Raleway-Bold;
+                font-size: 10px;
+                background-color: #426aad;
+                padding: 2px 6px 2px 6px;
+                text-decoration: none;
+            }
+            button.blueBtn:hover {
+                background-color: #1e5dc9;
+                color: #ffffff;
+                text-decoration: none;
+            }            
+            button.zoneSelectorButton {
+                margin: 0px;
+                width: 100%;
+                border: 0px;
+                color: #000000;
+                font-family: Raleway-Bold;
+                font-size: 12px;
+                background-color: #c0c0c0;
+                padding: 4px;
+                text-align: left;
+            }
+            button.zoneSelectorButton:hover {
+                background-color: #999999;
+            }
+            div.listContainer {
+                overflow-x: hidden;
+                overflow-y: auto;
+                width: 100%;
+                height: 450px;
+            }
+        </style>
+    </head>
+    <body>
+        <div id="rwzmUI">
+            <div style='text-align: center; width: 100%;'>
+                <h1>
+                    <br>
+                    <br>
+                    Analysis in progress...
+                </h1>
+                <br>
+                <img src='../assets/images/processing.gif'>
+            </div>
+        </div>
+    </body>
+    <script>
+        var enforceLocked = false;
+        var highlightedID = "";
+        var entitiesToAddTo = [];
+        var targetZoneID = "";
+    
+        EventBridge.scriptEventReceived.connect(function (message) {
+            document.getElementById("rwzmUI").innerHTML = message;
+        });
+        
+        function highlight(id) {
+            EventBridge.emitWebEvent(JSON.stringify({
+                "action": "highlight",
+                "id": id,
+                "enforceLocked": document.getElementById("enforceLocked").checked
+            }));
+        }
+        
+        function getIdsFromScope() {
+            var ids = [];
+            var i = 0;
+            var checkboxes = document.getElementsByName("entitiesScope");
+            for(i = 0; i < checkboxes.length; i++) {  
+                if(checkboxes[i].checked) {
+                    ids.push(checkboxes[i].value);  
+                }
+            }
+            return ids;
+        }
+        
+        function selectAllOrNone() {
+            var setTo = false;
+            if (document.getElementById("fullScope").checked) {
+                setTo = true;
+            }
+            var checkboxes = document.getElementsByName("entitiesScope");
+            for(i = 0; i < checkboxes.length; i++) {  
+                checkboxes[i].checked = setTo;
+            }
+        }
+        
+        function addZoneToEntity(ids){
+            if (ids.length === 0) {
+                return;
+            } else {
+                enforceLocked = document.getElementById("enforceLocked").checked;
+                highlightedID = document.getElementById("highlightedID").value;
+                entitiesToAddTo = ids;
+                document.body.style.overflow = "hidden";
+                document.getElementById("rwzmAddZoneSelector").style.display = "block";
+            }
+        }
+        
+        function cancelZoneSelector() {
+            document.getElementById("rwzmAddZoneSelector").style.display = "none";
+            document.getElementById("rwzmReplaceZoneSelector").style.display = "none";
+            document.body.style.overflow = "auto";
+        }
+        
+        function addThisZone(id) {
+            document.getElementById("rwzmAddZoneSelector").style.display = "none";
+            document.body.style.overflow = "auto";
+            EventBridge.emitWebEvent(JSON.stringify({
+                "action": "addZoneToEntities",
+                "ids": entitiesToAddTo,
+                "zoneID": id,
+                "enforceLocked": enforceLocked,
+                "highlightedID": highlightedID
+            }));
+        }
+        
+        function removeZoneFromRWZ(id, rwzId) {
+            EventBridge.emitWebEvent(JSON.stringify({
+                "action": "removeZoneFromEntity",
+                "id": id,
+                "zoneID": rwzId,
+                "enforceLocked": document.getElementById("enforceLocked").checked,
+                "highlightedID": document.getElementById("highlightedID").value
+            }));
+        }
+
+        function replaceZoneOnAllEntities(id) {
+            var goProceed = false;
+            if (!document.getElementById("enforceLocked").checked) {
+                goProceed = confirm("Locked entities won't be modified unless you check the option 'Modify locked entities for me'\nAre you sure you want to do this?");
+            } else {
+                goProceed = true;
+            }
+            if (goProceed) {
+                enforceLocked = document.getElementById("enforceLocked").checked;
+                highlightedID = document.getElementById("highlightedID").value;
+                targetZoneID = id;
+                document.body.style.overflow = "hidden";
+                document.getElementById("rwzmReplaceZoneSelector").style.display = "block";
+            }
+        }
+        
+        function replaceByThisZone(replacementZoneID){
+            document.getElementById("rwzmReplaceZoneSelector").style.display = "none";
+            document.body.style.overflow = "auto";
+            EventBridge.emitWebEvent(JSON.stringify({
+                "action": "replaceZoneOnAllEntities",
+                "targetZoneID": targetZoneID,
+                "replacementZoneID": replacementZoneID,
+                "enforceLocked": enforceLocked,
+                "highlightedID": highlightedID
+            }));
+        }
+        
+        function removeZoneOnAllEntities(id) {
+            var goProceed = false;
+            if (!document.getElementById("enforceLocked").checked) {
+                goProceed = confirm("Locked entities won't be modified unless you check the option 'Modify locked entities for me'\nAre you sure you want to do this?");
+            } else {
+                goProceed = true;
+            }
+            if (goProceed) {
+                EventBridge.emitWebEvent(JSON.stringify({
+                    "action": "removeZoneOnAllEntities",
+                    "zoneID": id,
+                    "enforceLocked": document.getElementById("enforceLocked").checked,
+                    "highlightedID": document.getElementById("highlightedID").value
+                }));
+            }
+        }
+        
+        function undo() {
+            EventBridge.emitWebEvent(JSON.stringify({
+                "action": "undo",
+                "enforceLocked": document.getElementById("enforceLocked").checked,
+                "highlightedID": document.getElementById("highlightedID").value
+            }));
+        }
+        
+        function refresh() {
+            EventBridge.emitWebEvent(JSON.stringify({
+                "action": "refresh",
+                "enforceLocked": document.getElementById("enforceLocked").checked,
+                "highlightedID": document.getElementById("highlightedID").value
+            }));
+        }
+    </script>
+</html>
diff --git a/scripts/system/create/modules/renderWithZonesManager.js b/scripts/system/create/modules/renderWithZonesManager.js
new file mode 100644
index 0000000000..49dec2bdf3
--- /dev/null
+++ b/scripts/system/create/modules/renderWithZonesManager.js
@@ -0,0 +1,453 @@
+//
+//  renderWithZonesManager.js
+//
+//  Created by Alezia Kurdis on January 28th, 2024.
+//  Copyright 2024 Overte e.V.
+//
+//  This script is to manage the zone in the property renderWithZones more efficiently in the Create Application.
+//  It allows a global view over a specific selection with possibility to 
+//  REPLACE, REMOVE or ADD zones on those properties more efficiently.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+let rwzmSelectedId = [];
+let rwzmAllZonesList = [];
+let rwzmUsedZonesList = [];
+let rwzmSelectedEntitiesData = [];
+let rwzmUndo = [];
+
+let enforceLocked = false;
+
+const RWZ_ZONE_SCAN_RADIUS = 27713; //maximal radius to cover the entire domain.
+
+let rwzmOverlayWebWindow = null;
+
+function renderWithZonesManager(entityIDs, highlightedID = "") {
+    if (rwzmGetCheckSum(entityIDs) !== rwzmGetCheckSum(rwzmSelectedId)) {
+        rwzmUndo = [];
+    }
+    rwzmSelectedId = entityIDs;
+    if (entityIDs.length === 0) {
+        audioFeedback.rejection();
+        Window.alert("You have nothing selected.");
+        return;
+    } else {
+        rwzmAllZonesList = [];
+        rwzmUsedZonesList = [];
+        rwzmSelectedEntitiesData = [];
+        rwzmAllZonesList = rwzmGetExistingZoneList();
+        
+        let properties;
+        let i = 0;
+        let j = 0;
+        let rwzmData = {};
+        for (i = 0; i < entityIDs.length; i++ ){
+            properties = Entities.getEntityProperties(entityIDs[i], ["renderWithZones", "locked", "name", "type"]);
+            //Identify the unique zone used in renderWithZones properties of the entities and make the list of this in rwzmUsedZonesList
+            if (properties.renderWithZones.length > 0) {
+                for (j = 0; j < properties.renderWithZones.length; j++ ){
+                    if (rwzmUsedZonesList.indexOf(properties.renderWithZones[j]) === -1) {
+                        rwzmUsedZonesList.push(properties.renderWithZones[j]);
+                    }
+                }
+            }
+            //Make the list of entities, with their id, renderWithZones, locked, in rwzmSelectedEntitiesData
+            rwzmData = {
+                "id": entityIDs[i],
+                "name": properties.name,
+                "type": properties.type,
+                "renderWithZones": properties.renderWithZones,
+                "locked": properties.locked
+            };
+            rwzmSelectedEntitiesData.push(rwzmData);
+        }
+    }
+
+    if (rwzmOverlayWebWindow === null) {
+        rwzmOverlayWebWindow = new OverlayWebWindow({
+            title: "RenderWithZones Manager",
+            source: Script.resolvePath("renderWithZonesManager.html"),
+            width: 1100,
+            height: 600
+        });
+    
+        rwzmOverlayWebWindow.closed.connect(uiHasClosed);
+        rwzmOverlayWebWindow.webEventReceived.connect(webEventReceiver);
+    }
+    
+    rwzmGenerateUI(highlightedID);
+}
+
+function rwzmGetCheckSum(array) {
+    let i = 0;
+    let sum = 0;
+    let strForm = JSON.stringify(array);
+    for (i = 0; i < strForm.length; i++) {
+        sum = sum + strForm.charCodeAt(i);
+    }
+    return sum;
+}
+function uiHasClosed() {
+    rwzmOverlayWebWindow.closed.disconnect(uiHasClosed);
+    rwzmOverlayWebWindow.webEventReceived.disconnect(webEventReceiver);
+    rwzmOverlayWebWindow = null;
+}
+
+function rwzmGenerateUI(highlightedID) {
+    let canUnlock = Entities.canAdjustLocks();
+    let uiContent = "";
+    let i = 0;
+    let k = 0;
+    let zones = "";
+    let name = "";
+    let elementClass = "";
+    let firstClass = "";
+    let isLocked = "";
+    let selectionBox = "";
+    let setCheck = "";
+    let addAction = "";
+    let toHighlight = "";
+    let viewBtnCaption = "";
+    let warning = false;
+    uiContent = uiContent + '    <h1>RenderWithZones Manager</h1><hr>\n';
+    if (canUnlock) {
+        if (enforceLocked) {
+            setCheck = " checked";
+        } else {
+            setCheck = "";
+        }
+    }
+    uiContent = uiContent + '    <table>\n';
+    uiContent = uiContent + '        <tr valign = "top">\n';
+    uiContent = uiContent + '            <td style="width: 40%">\n';
+    if (rwzmUndo.length === 0) {
+        undoBtnCode = "";
+    } else {
+        undoBtnCode = '<button class="greyBtn small" onClick="undo();">Undo</button>';
+    }
+    uiContent = uiContent + '                <table><tr><td style="width: 40%;"><h2>Visibility Zones:</h2></td><td style="width: 60%; text-align: right;">' + undoBtnCode + '</td></tr></table>\n';
+    uiContent = uiContent + '                <div class="listContainer"><table>\n';
+    uiContent = uiContent + '                    <tr style="width: 100%"><td class="cells header" style="width: 60%;">';
+    uiContent = uiContent + '<b>ZONES</b></td><td class="cells header" style="width: 40%;"><b>ACTIONS (On listed entities)</b></td></tr>\n';
+    for (i = 0; i < rwzmUsedZonesList.length; i++ ) {
+        name = rwzmGetZoneName(rwzmUsedZonesList[i]);
+        elementClass = "line";
+        firstClass = "cells";
+        if (name === "") {
+            name = rwzmGenerateUnidentifiedZoneName(rwzmUsedZonesList[i]);
+            elementClass = "errorline";
+            warning = true;
+        }
+        toHighlight = rwzmUsedZonesList[i];
+        viewBtnCaption = "View";
+        if ( rwzmUsedZonesList[i] === highlightedID) {
+            toHighlight = "";
+            viewBtnCaption = "Hide";
+            firstClass = "highlightedCells";
+            if (elementClass === "line") {
+                elementClass = "lineInverted";
+            }
+        }
+        uiContent = uiContent + '                    <tr style="width: 100%"><td class="' + firstClass + ' ' + elementClass;
+        uiContent = uiContent + '"><div style = "width:100%; height:100%; padding: 3px;" onClick = "highlight(' + "'";
+        uiContent = uiContent + toHighlight + "'" +');">' + rwzmGetTruncatedString(name, 30) + '</div></td><td class="' + firstClass + ' ' + elementClass + '">';
+        uiContent = uiContent + '<button class="greyBtn small" onclick="highlight(' + "'" + toHighlight + "'" +');">' + viewBtnCaption + '</button>';
+        uiContent = uiContent + '<button class="redBtn" onclick="removeZoneOnAllEntities(' + "'" + rwzmUsedZonesList[i] + "'" +');">Remove</button>';
+        uiContent = uiContent + '<button class="blueBtn" onclick="replaceZoneOnAllEntities(' + "'" + rwzmUsedZonesList[i] + "'" +');">Replace</button></td></tr>\n';
+    }
+    uiContent = uiContent + '                </table></div>\n';
+    uiContent = uiContent + '            </td>\n';
+    uiContent = uiContent + '            <td style="width: 3%">&nbsp;</td>\n';
+    uiContent = uiContent + '            <td style="width: 57%">\n';
+    uiContent = uiContent + '                <table><tr><td style="width: 15%;"><h2>Entities:</h2></td><td style="width: 30%;"><button class="addbtn" onClick="addZoneToEntity(getIdsFromScope());">Add to Selected</button></td>';
+    uiContent = uiContent + '<td style="width: 55%; text-align: right;"><input type="checkbox" id = "enforceLocked"'+ setCheck + ' onClick="refresh()";> <font style="color: #3bc7ff;">Modify locked entities for me.</font></td></tr></table>\n';
+    uiContent = uiContent + '                <div class="listContainer"><table>\n';
+    uiContent = uiContent + '                    <tr style="width: 100%;"><td class="cells header" style="width: 5%;">';
+    uiContent = uiContent + '<input type="checkbox" id="fullScope" onClick="selectAllOrNone();"></td><td class="cells header" style="width: 3%;">';
+    uiContent = uiContent + '<font class="hifiGlyphs">&#xe006;</font></td><td class="cells header" style = "width: 45%;"><b>ENTITIES</b></td><td class="cells header" style = "width: 40%;">';
+    uiContent = uiContent + '<b>RENDER WITH ZONES</b></td><td style = "width: 7%;"class="cells header">&nbsp;</td></tr>\n';
+    for (i = 0; i < rwzmSelectedEntitiesData.length; i++ ) {
+        elementClass = "line";
+        firstClass = "cells";
+        if (rwzmSelectedEntitiesData[i].renderWithZones.indexOf(highlightedID) !== -1) {
+            firstClass = "highlightedCells";
+            elementClass = "lineInverted";
+        }
+        zones = "&nbsp;";
+        if (rwzmSelectedEntitiesData[i].renderWithZones.length > 0) {
+            for (k = 0; k < rwzmSelectedEntitiesData[i].renderWithZones.length; k++ ) {
+                name = rwzmGetTruncatedString(rwzmGetZoneName(rwzmSelectedEntitiesData[i].renderWithZones[k]),30);
+                if (name === "") {
+                    name = rwzmGetTruncatedString(rwzmGenerateUnidentifiedZoneName(rwzmSelectedEntitiesData[i].renderWithZones[k]),30);
+                }
+                if ((canUnlock && enforceLocked && rwzmSelectedEntitiesData[i].locked) || !rwzmSelectedEntitiesData[i].locked) {
+                    name = name + " <span class='delBtn' onClick='removeZoneFromRWZ(" + '"' + rwzmSelectedEntitiesData[i].id + '", "' + rwzmSelectedEntitiesData[i].renderWithZones[k] + '"' + ");'>&#11198;</span>";
+                }
+                
+                if (k === 0) {
+                    zones = zones + name;
+                } else {
+                    zones = zones + "<br>" + name;
+                }
+            }
+        }
+        isLocked = "&nbsp;";
+        selectionBox = "&nbsp;";
+        addAction = "&nbsp;";
+        if ((canUnlock && enforceLocked && rwzmSelectedEntitiesData[i].locked) || !rwzmSelectedEntitiesData[i].locked) {
+            addAction = "<button class='addbtn' onClick='addZoneToEntity([" + '"' + rwzmSelectedEntitiesData[i].id + '"' + "]);'>Add</button>";
+            selectionBox = '<input type="checkbox" name="entitiesScope" value = "' + rwzmSelectedEntitiesData[i].id + '">';
+        }
+        if (rwzmSelectedEntitiesData[i].locked) {
+            if (canUnlock) {
+                isLocked = "&#xe006;"; //Locked
+            } else {
+                isLocked = "&#128711;"; //Forbidden
+            }
+        }
+        uiContent = uiContent + '                    <tr style="width: 100%"><td class="' + firstClass + ' ' + elementClass + '">' + selectionBox;
+        uiContent = uiContent + '</td><td class="' + firstClass + ' ' + elementClass + '"><font class="hifiGlyphs">' + isLocked + '</font></td><td class="';
+        uiContent = uiContent + firstClass + ' ' + elementClass + '">' + rwzmSelectedEntitiesData[i].type + ' - ' + rwzmGetTruncatedString(rwzmSelectedEntitiesData[i].name, 30) + '</b></td><td class="' + firstClass;
+        uiContent = uiContent + ' ' + elementClass + '">' + zones + '</td><td class="' + firstClass + ' ' + elementClass + '">' + addAction + '</td></tr>\n';
+    }
+    uiContent = uiContent + '                </table>\n';
+    uiContent = uiContent + '            </td>\n';
+    uiContent = uiContent + '        </tr>\n';
+    uiContent = uiContent + '    </table></div>\n';
+    uiContent = uiContent + '    <input type = "hidden" id = "highlightedID" value = "' + highlightedID + '">\n';
+    if (warning) {
+        uiContent = uiContent + '    <div class="warning"><b>WARNING</b>: The "<b>ZONE NOT FOUND</b>" visibility zones might simply not be loaded if too far and small. Please, verify before.</div>\n';
+    }
+    //Zone selector Add
+    uiContent = uiContent + '    <div id="rwzmAddZoneSelector">\n';
+    uiContent = uiContent + '    <h2>Select the zone to add:</h2><div class="zoneSelectorContainer">\n';
+    for (i = 0; i < rwzmAllZonesList.length; i++ ) {
+        uiContent = uiContent + "        <button class = 'zoneSelectorButton' onClick='addThisZone(" + '"' + rwzmAllZonesList[i].id + '"' + ");'>" + rwzmAllZonesList[i].name + "</button><br>\n";
+    }
+    uiContent = uiContent + '        </div><div style="width: 98%; text-align: right;"><button class = "greyBtn" onclick="cancelZoneSelector();">Cancel</button></div>\n';
+    uiContent = uiContent + '    </div>\n';
+    //Zone selector Replace
+    uiContent = uiContent + '    <div id="rwzmReplaceZoneSelector">\n';
+    uiContent = uiContent + '    <h2>Select the replacement zone:</h2><div class="zoneSelectorContainer">\n';
+    for (i = 0; i < rwzmAllZonesList.length; i++ ) {
+        uiContent = uiContent + "        <button class = 'zoneSelectorButton' onClick='replaceByThisZone(" + '"' + rwzmAllZonesList[i].id + '"' + ");'>" + rwzmAllZonesList[i].name + "</button><br>\n";
+    }
+    uiContent = uiContent + '        </div><div style="width: 98%; text-align: right;"><button class = "greyBtn" onclick="cancelZoneSelector();">Cancel</button></div>\n';
+    uiContent = uiContent + '    </div>\n';
+    
+    Script.setTimeout(function () {
+        rwzmOverlayWebWindow.emitScriptEvent(uiContent);
+    }, 300);
+}
+
+function rwzmGetZoneName(id) {
+    let k = 0;
+    let name = "";
+    for (k = 0; k < rwzmAllZonesList.length; k++) {
+        if (rwzmAllZonesList[k].id === id) {
+            name = rwzmAllZonesList[k].name;
+            break;
+        }
+    }
+    return name;
+}
+
+function rwzmGenerateUnidentifiedZoneName(id) {
+    let partialID = id.substr(1,8);
+    return "ZONE NOT FOUND (" + partialID +")";
+}
+
+function rwzmGetExistingZoneList() {
+    var center = { "x": 0, "y": 0, "z": 0 };
+    var existingZoneIDs = Entities.findEntitiesByType("Zone", center, RWZ_ZONE_SCAN_RADIUS);
+    var listExistingZones = [];
+    var thisZone = {};
+    var properties;
+    for (var k = 0; k < existingZoneIDs.length; k++) {
+        properties = Entities.getEntityProperties(existingZoneIDs[k], ["name"]);
+        thisZone = {
+            "id": existingZoneIDs[k],
+            "name": properties.name
+        };
+        listExistingZones.push(thisZone);
+    }
+    listExistingZones.sort(rwzmZoneSortOrder);
+    return listExistingZones;
+}
+
+function rwzmZoneSortOrder(a, b) {
+    var nameA = a.name.toUpperCase();
+    var nameB = b.name.toUpperCase();
+    if (nameA > nameB) {
+        return 1;
+    } else if (nameA < nameB) {
+        return -1;
+    }
+    if (a.name > b.name) {
+        return 1;
+    } else if (a.name < b.name) {
+        return -1;
+    }
+    return 0;
+}
+
+function rwzmRemoveZoneFromEntity(id, zoneID, forceLocked, highlightedID) {
+    rwzmUndo = [];
+    let properties = Entities.getEntityProperties(id, ["renderWithZones", "locked"]);
+    
+    let newRenderWithZones = [];
+    let i = 0;
+    for (i = 0; i < properties.renderWithZones.length; i++) {
+        if (properties.renderWithZones[i] !== zoneID) {
+            newRenderWithZones.push(properties.renderWithZones[i]);
+        }
+    }
+    
+    if (forceLocked && properties.locked) {
+        Entities.editEntity(id, {"locked": false});
+        Entities.editEntity(id, {"renderWithZones": newRenderWithZones, "locked": properties.locked});
+    } else {
+        Entities.editEntity(id, {"renderWithZones": newRenderWithZones});
+    }
+    rwzmUndo.push({"id": id, "renderWithZones": properties.renderWithZones});
+    renderWithZonesManager(rwzmSelectedId, highlightedID);
+}
+
+function rwzmAddZonesToEntities(ids, zoneID, forceLocked, highlightedID) {
+    rwzmUndo = [];
+    let k = 0;
+    let j = 0;
+    let properties;
+    let newRenderWithZones = [];
+    for (k = 0; k < ids.length; k++) {
+        properties = Entities.getEntityProperties(ids[k], ["renderWithZones", "locked"]);
+        newRenderWithZones = [];
+        
+        for (j = 0; j < properties.renderWithZones.length; j++) {
+            if (properties.renderWithZones[j] !== zoneID) {
+                newRenderWithZones.push(properties.renderWithZones[j]);
+            }
+        }
+        newRenderWithZones.push(zoneID);
+        if (forceLocked && properties.locked) {
+            Entities.editEntity(ids[k], {"locked": false});
+            Entities.editEntity(ids[k], {"renderWithZones": newRenderWithZones, "locked": properties.locked});
+        } else {
+            Entities.editEntity(ids[k], {"renderWithZones": newRenderWithZones});
+        }
+        rwzmUndo.push({"id": ids[k], "renderWithZones": properties.renderWithZones});
+    }
+    renderWithZonesManager(rwzmSelectedId, highlightedID);
+}
+
+function rwzmRemoveZoneFromAllEntities(zoneID, forceLocked, highlightedID) {
+    rwzmUndo = [];
+    let k = 0;
+    let j = 0;
+    let properties;
+    let newRenderWithZones = [];
+    for (k = 0; k < rwzmSelectedId.length; k++) {
+        properties = Entities.getEntityProperties(rwzmSelectedId[k], ["renderWithZones", "locked"]);
+        newRenderWithZones = [];
+        
+        for (j = 0; j < properties.renderWithZones.length; j++) {
+            if (properties.renderWithZones[j] !== zoneID) {
+                newRenderWithZones.push(properties.renderWithZones[j]);
+            }
+        }
+        if (forceLocked && properties.locked) {
+            Entities.editEntity(rwzmSelectedId[k], {"locked": false});
+            Entities.editEntity(rwzmSelectedId[k], {"renderWithZones": newRenderWithZones, "locked": properties.locked});
+        } else {
+            Entities.editEntity(rwzmSelectedId[k], {"renderWithZones": newRenderWithZones});
+        }
+        rwzmUndo.push({"id": rwzmSelectedId[k], "renderWithZones": properties.renderWithZones});
+    }
+    renderWithZonesManager(rwzmSelectedId, highlightedID);
+}
+
+function rwzmReplaceZoneOnAllEntities(targetZoneID, replacementZoneID, forceLocked, highlightedID) {
+    rwzmUndo = [];
+    let k = 0;
+    let j = 0;
+    let properties;
+    let newRenderWithZones = [];
+    for (k = 0; k < rwzmSelectedId.length; k++) {
+        properties = Entities.getEntityProperties(rwzmSelectedId[k], ["renderWithZones", "locked"]);
+        newRenderWithZones = [];
+        
+        for (j = 0; j < properties.renderWithZones.length; j++) {
+            if (properties.renderWithZones[j] !== targetZoneID) {
+                newRenderWithZones.push(properties.renderWithZones[j]);
+            } else {
+                newRenderWithZones.push(replacementZoneID);
+            }
+        }
+        if (forceLocked && properties.locked) {
+            Entities.editEntity(rwzmSelectedId[k], {"locked": false});
+            Entities.editEntity(rwzmSelectedId[k], {"renderWithZones": newRenderWithZones, "locked": properties.locked});
+        } else {
+            Entities.editEntity(rwzmSelectedId[k], {"renderWithZones": newRenderWithZones});
+        }
+        rwzmUndo.push({"id": rwzmSelectedId[k], "renderWithZones": properties.renderWithZones});
+    }
+    renderWithZonesManager(rwzmSelectedId, highlightedID);
+}
+
+function rwzmGetTruncatedString(str, max) {
+    if (str.length > max) {
+        return str.substr(0, max-1) + "&#8230;";
+    } else {
+        return str;
+    }
+}
+
+function rwzmUndoLastAction(highlightedID) {
+    let k = 0;
+    let properties;
+    let locked;
+    for (k = 0; k < rwzmUndo.length; k++) {
+        locked = Entities.getEntityProperties(rwzmUndo[k].id, ["locked"]).locked;
+        if (locked) {
+            Entities.editEntity(rwzmUndo[k].id, {"locked": false});
+            Entities.editEntity(rwzmUndo[k].id, {"renderWithZones": rwzmUndo[k].renderWithZones, "locked": locked});
+        } else {
+            Entities.editEntity(rwzmUndo[k].id, {"renderWithZones": rwzmUndo[k].renderWithZones});
+        }
+    }
+    rwzmUndo = [];
+    renderWithZonesManager(rwzmSelectedId, highlightedID);
+}
+
+function webEventReceiver (message) {
+    try {
+        var data = JSON.parse(message);
+    } catch(e) {
+        print("renderWithZonesManager.js: Error parsing JSON");
+        return;
+    }
+    if (data.action === "highlight") {
+        enforceLocked = data.enforceLocked;
+        renderWithZonesManager(rwzmSelectedId, data.id);
+    } else if (data.action === "removeZoneFromEntity") {
+        enforceLocked = data.enforceLocked;
+        rwzmRemoveZoneFromEntity(data.id, data.zoneID, data.enforceLocked, data.highlightedID);
+    } else if (data.action === "addZoneToEntities") {
+        enforceLocked = data.enforceLocked;
+        rwzmAddZonesToEntities(data.ids, data.zoneID, data.enforceLocked, data.highlightedID);
+    } else if (data.action === "refresh") {
+        enforceLocked = data.enforceLocked;
+        renderWithZonesManager(rwzmSelectedId, data.highlightedID);
+    } else if (data.action === "removeZoneOnAllEntities") {
+        enforceLocked = data.enforceLocked;
+        rwzmRemoveZoneFromAllEntities(data.zoneID, data.enforceLocked, data.highlightedID);
+    } else if (data.action === "replaceZoneOnAllEntities") {
+        enforceLocked = data.enforceLocked;
+        rwzmReplaceZoneOnAllEntities(data.targetZoneID, data.replacementZoneID, data.enforceLocked, data.highlightedID);
+    } else if (data.action === "undo") {
+        enforceLocked = data.enforceLocked;
+        rwzmUndoLastAction(data.highlightedID);
+    }
+}
+
diff --git a/scripts/system/create/qml/EditTabView.qml b/scripts/system/create/qml/EditTabView.qml
index 96e66c109e..2db23ec659 100644
--- a/scripts/system/create/qml/EditTabView.qml
+++ b/scripts/system/create/qml/EditTabView.qml
@@ -301,6 +301,22 @@ TabBar {
         }
     }
 
+    EditTabButton {
+        title: "IMPORT"
+        active: true
+        enabled: true
+        property string originalUrl: ""
+
+        property Component visualItem: Component {
+            WebView {
+                id: advancedImportWebView
+                url: Qt.resolvedUrl("../importEntities/html/importEntities.html")
+                enabled: true
+                blurOnCtrlShift: false
+            }
+        }
+    }
+
     function fromScript(message) {
         switch (message.method) {
             case 'selectTab':
@@ -333,6 +349,9 @@ TabBar {
                 case 'grid':
                     editTabView.currentIndex = 3;
                     break;
+                case 'import':
+                    editTabView.currentIndex = 4;
+                    break;
                 default:
                     console.warn('Attempt to switch to invalid tab:', id);
             }
diff --git a/scripts/system/create/qml/EditToolsTabView.qml b/scripts/system/create/qml/EditToolsTabView.qml
index 998c3a3aac..1000724458 100644
--- a/scripts/system/create/qml/EditToolsTabView.qml
+++ b/scripts/system/create/qml/EditToolsTabView.qml
@@ -291,6 +291,22 @@ TabBar {
         }
     }
 
+    EditTabButton {
+        title: "IMPORT"
+        active: true
+        enabled: true
+        property string originalUrl: ""
+
+        property Component visualItem: Component {
+            WebView {
+                id: advancedImportWebView
+                url: Qt.resolvedUrl("../importEntities/html/importEntities.html")
+                enabled: true
+                blurOnCtrlShift: false
+            }
+        }
+    }
+
     function fromScript(message) {
         switch (message.method) {
             case 'selectTab':
@@ -304,7 +320,7 @@ TabBar {
     // Changes the current tab based on tab index or title as input
     function selectTab(id) {
         if (typeof id === 'number') {
-            if (id >= tabIndex.create && id <= tabIndex.grid) {
+            if (id >= tabIndex.create && id <= tabIndex.import) {
                 editTabView.currentIndex = id;
             } else {
                 console.warn('Attempt to switch to invalid tab:', id);
@@ -320,6 +336,9 @@ TabBar {
                 case 'grid':
                     editTabView.currentIndex = tabIndex.grid;
                     break;
+                case 'import':
+                    editTabView.currentIndex = tabIndex.import;
+                    break;
                 default:
                     console.warn('Attempt to switch to invalid tab:', id);
             }
diff --git a/scripts/system/emote.js b/scripts/system/emote.js
index 6dfd1ae1ef..0d56932b4b 100644
--- a/scripts/system/emote.js
+++ b/scripts/system/emote.js
@@ -15,6 +15,7 @@
 
 (function() { // BEGIN LOCAL_SCOPE
 
+var controllerStandard = Controller.Standard;
 
 var EMOTE_ANIMATIONS = 
     ['Crying', 'Surprised', 'Dancing', 'Cheering', 'Waving', 'Fall', 'Pointing', 'Clapping', 'Sit1', 'Sit2', 'Sit3', 'Love'];
@@ -138,22 +139,22 @@ function restoreAnimation() {
 }
                     
 // Note peek() so as to not interfere with other mappings.
-eventMapping.from(Controller.Standard.LeftPrimaryThumb).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.RightPrimaryThumb).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.LeftSecondaryThumb).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.RightSecondaryThumb).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.LB).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.LS).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.RY).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.RX).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.LY).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.LX).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.LeftGrip).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.RB).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.RS).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.RightGrip).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.Back).peek().to(restoreAnimation);
-eventMapping.from(Controller.Standard.Start).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.LeftPrimaryThumb).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.RightPrimaryThumb).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.LeftSecondaryThumb).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.RightSecondaryThumb).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.LB).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.LS).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.RY).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.RX).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.LY).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.LX).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.LeftGrip).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.RB).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.RS).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.RightGrip).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.Back).peek().to(restoreAnimation);
+eventMapping.from(controllerStandard.Start).peek().to(restoreAnimation);
 
 
 button.clicked.connect(onClicked);
diff --git a/scripts/system/fingerPaint.js b/scripts/system/fingerPaint.js
index 88245503e8..376a60e85d 100644
--- a/scripts/system/fingerPaint.js
+++ b/scripts/system/fingerPaint.js
@@ -9,6 +9,8 @@
 //
 
 (function () {
+    var controllerStandard = Controller.Standard;
+
     var tablet,
         button,
         BUTTON_NAME = "PAINT",
@@ -334,11 +336,11 @@
         leftHand = handController("left");
         rightHand = handController("right");
         var controllerMapping = Controller.newMapping(CONTROLLER_MAPPING_NAME);
-        controllerMapping.from(Controller.Standard.LT).to(leftHand.onTriggerPress);
-        controllerMapping.from(Controller.Standard.LeftGrip).to(leftHand.onGripPress);
-        controllerMapping.from(Controller.Standard.RT).to(rightHand.onTriggerPress);
-        controllerMapping.from(Controller.Standard.RightGrip).to(rightHand.onGripPress);
-        controllerMapping.from(Controller.Standard.B).to(onButtonClicked);
+        controllerMapping.from(controllerStandard.LT).to(leftHand.onTriggerPress);
+        controllerMapping.from(controllerStandard.LeftGrip).to(leftHand.onGripPress);
+        controllerMapping.from(controllerStandard.RT).to(rightHand.onTriggerPress);
+        controllerMapping.from(controllerStandard.RightGrip).to(rightHand.onGripPress);
+        controllerMapping.from(controllerStandard.B).to(onButtonClicked);
         Controller.enableMapping(CONTROLLER_MAPPING_NAME);
         
         if (!Settings.getValue("FingerPaintTutorialComplete")) {
diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css
index 4a87df5659..27f1ce23f3 100644
--- a/scripts/system/html/css/edit-style.css
+++ b/scripts/system/html/css/edit-style.css
@@ -1,10 +1,10 @@
 /*
 //  edit-style.css
 //
-//  Created by Ryan Huffman on 13 Nov 2014
+//  Created by Ryan Huffman on November 13th, 2014
 //  Copyright 2014 High Fidelity, Inc.
 //  Copyright 2020 Vircadia contributors.
-//  Copyright 2022 Overte e.V.
+//  Copyright 2022-2024 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
@@ -701,6 +701,17 @@ div.section-header, hr {
     align-items: center;
 }
 
+div.simpleSeparator {
+    width: 97%;
+    height: 2px;
+    border: 0px;
+    margin-top: 4px;
+    margin-right: 8px;
+    margin-left: 8px;
+    margin-bottom: 4px;
+    background-color: #777777;
+}
+
 .section.minor {
     margin: 0 21px;
     box-shadow: 1px -1px 0 rgb(37,37,37);
@@ -1304,6 +1315,27 @@ div#grid-section, body#entity-list-body {
     margin: 0px 8px 8px 8px;
 }
 
+#voxels-section {
+    padding-bottom: 0px;
+    margin: 8px 8px 8px 8px;
+}
+
+#mode-section {
+    padding: 3px;
+    height: 28px;
+    margin: 8px 8px 8px 8px;
+    color: #000000;
+    background-color: #999999;
+    font-family: Raleway-Bold;
+    font-size: 16px;
+    border-radius: 5px;
+}
+#creationModeLabel {
+    padding-top: 4px;
+    width: 156px;
+}
+
+
 #entity-list-header {
     margin-bottom: 6px;
 }
@@ -1433,6 +1465,11 @@ input[type=button].entity-list-menutitle {
     background: linear-gradient(#343434 30%, #000 100%);
     cursor: pointer;
 }
+
+#voxel-edit-mode {
+    width: 120px;
+}
+
 input[type=button].entity-list-menutitle:enabled:hover {
     background: linear-gradient(#000, #000);
     border: none;
@@ -1743,6 +1780,7 @@ input#property-scale-button-reset {
     display: none;
     position: fixed;
     color: #000000;
+    font-family: FiraSans-SemiBold;
     background-color: #afafaf;
     padding: 5px 0 5px 0;
     cursor: default;
@@ -1762,7 +1800,7 @@ input#property-scale-button-reset {
     padding: 0 0;
 }
 .context-menu li.disabled {
-    color: #333333;
+    color: #777777;
 }
 .context-menu li.separator:hover, .context-menu li.disabled:hover {
     background-color: #afafaf;
@@ -2080,10 +2118,10 @@ div.entity-list-menu {
 div.tools-select-menu {
     position: relative;
     display: none;
-    width: 370px;
+    width: 200px;
     height: 0px;
-    top: 0px;
-    left: 8px;
+    top: -16px;
+    left: 124px;
     right: 0;
     bottom: 0;
     border-style: solid;
@@ -2095,20 +2133,19 @@ div.tools-select-menu {
 }
 
 div.tools-help-popup {
-    font-family: FiraSans-SemiBold;
-    font-size: 15px;
+    font-family: Raleway-SemiBold;
+    font-size: 14px;
     position: relative;
     display: none;
-    width: 690px;
+    width: 96%;
+    padding: 5px;
     height: auto;
-    top: 0px;
+    top: -16px;
     left: 8px;
     right: 0;
     bottom: 0;
-    border-style: solid;
-    border-color: #505050;
-    border-width: 1px;
-    background-color: #404040;
+    border: 2px solid #c0c0c0;
+    background-color: #333333;
     z-index: 2;
     cursor: pointer;
 }
diff --git a/scripts/system/html/gridControls.html b/scripts/system/html/gridControls.html
index f752ec4f2a..e8cb643e46 100644
--- a/scripts/system/html/gridControls.html
+++ b/scripts/system/html/gridControls.html
@@ -1,9 +1,9 @@
 <!--
 //  gridControls.html
 //
-//  Created by Ryan Huffman on 6 Nov 2014
+//  Created by Ryan Huffman on November 6th, 2014
 //  Copyright 2014 High Fidelity, Inc.
-//  Copyright 2022 Overte e.V.
+//  Copyright 2022-2024 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
@@ -23,26 +23,28 @@
     <body onload='loaded();'>
         <div id="mode-section" class="section">
             <div class="property container">
-                <label for="create-app-mode">Create app mode </label>
+                <div id="creationModeLabel">
+                    CREATION MODE
+                </div>
                 <div class="property container">
                     <input name="create-app-mode" type="button" class="entity-list-menutitle" id="create-app-mode" value="Create app mode&#9662;" />
                 </div>
             </div>
-        <div class="tools-select-menu" id="edit-mode-menu" >
-            <button class="menu-button" id="edit-mode-object" >
-                <div class = "menu-item">
-                    <div class = "menu-item-caption">Object mode</div>
-                    <div class = "menu-item-shortcut"></div>
-                </div>
-            </button>
-            <button class="menu-button" id="edit-mode-voxel" >
-                <div class = "menu-item">
-                    <div class = "menu-item-caption">Voxel edit mode</div>
-                    <div class = "menu-item-shortcut"></div>
-                </div>
-            </button>
-        </div>        
-    </div>
+            <div class="tools-select-menu" id="edit-mode-menu" >
+                <button class="menu-button" id="edit-mode-object" >
+                    <div class = "menu-item">
+                        <div class = "menu-item-caption">Object mode</div>
+                        <div class = "menu-item-shortcut"></div>
+                    </div>
+                </button>
+                <button class="menu-button" id="edit-mode-voxel" >
+                    <div class = "menu-item">
+                        <div class = "menu-item-caption">Voxel edit mode</div>
+                        <div class = "menu-item-shortcut"></div>
+                    </div>
+                </button>
+            </div>
+        </div>
         <div id="voxels-section" class="section">
             <h2>Voxel edit settings</h2>
             <div class="property container">
@@ -76,9 +78,9 @@
                 </button>
             </div>
             <div class="property container">
-                <label for="voxel-remove">&nbsp;&nbsp;Remove voxels</label>
-                <div style="width: 100%">
-                    <input type='checkbox' id="voxel-remove" style="width: 100%">
+                <label for="voxel-remove">Remove voxels</label>
+                <div style="width: 100%;">
+                    <input type='checkbox' id="voxel-remove" style="width: 100%;">
                     <label for="voxel-remove">&nbsp;</label>
                 </div>
                 <div class="property container">
@@ -86,13 +88,14 @@
                 </div>
             </div>
             <div class="tools-help-popup" id="voxel-help-popup" >
-                <p>To edit voxels, Voxel Edit Mode needs to be selected.</p>
-                <p>Desktop mode:</p>
-                <p>Click the left mouse button to add a voxel. Click the middle mouse button to remove a voxel. Hold the mouse button and move the mouse to add or remove voxels in a single plane. The plane is determined by the direction in which you are looking when first voxel is added or removed (for example, look downwards to draw in horizontal plane).</p>
-                <p>VR mode:</p>
-                <p>Press the trigger to add a voxel. Press the trigger while holding the grip to remove a voxel. Hold the trigger and move the controller to add or remove voxels in a single plane. The plane is determined by the direction in which the controller's ray points when the first voxel is added or removed (for example point downwards to draw in the horizontal plane). Hold both grips and move your hands together or apart to change the size of the edit sphere.</p>
+                To edit voxels, "<b>Voxel Edit Mode</b>" needs to be selected.<br><br>
+                <b>Desktop:</b><br>
+                Click the left mouse button to add a voxel. Click the middle mouse button to remove a voxel. Hold the mouse button and move the mouse to add or remove voxels in a single plane. The plane is determined by the direction in which you are looking when first voxel is added or removed (for example, look downwards to draw in horizontal plane).<br><br>
+                <b>VR:</b><br>
+                Press the trigger to add a voxel. Press the trigger while holding the grip to remove a voxel. Hold the trigger and move the controller to add or remove voxels in a single plane. The plane is determined by the direction in which the controller's ray points when the first voxel is added or removed (for example point downwards to draw in the horizontal plane). Hold both grips and move your hands together or apart to change the size of the edit sphere.
             </div>
         </div>
+        <div class="simpleSeparator"></div>
         <div id="grid-section" class="section">
             <h2>Grid settings</h2>
             <div class="property container">
diff --git a/scripts/system/libraries/Trigger.js b/scripts/system/libraries/Trigger.js
index ffde021f5d..c59b60d5ea 100644
--- a/scripts/system/libraries/Trigger.js
+++ b/scripts/system/libraries/Trigger.js
@@ -6,6 +6,7 @@
 Trigger = function(properties) {
     properties = properties || {};
     var that = this;
+    var controllerStandard = Controller.Standard;
     that.label = properties.label || Math.random();
     that.SMOOTH_RATIO = properties.smooth || 0.1; //  Time averaging of trigger - 0.0 disables smoothing
     that.DEADZONE = properties.deadzone || 0.10; // Once pressed, a trigger must fall below the deadzone to be considered un-pressed once pressed.
@@ -44,8 +45,8 @@ Trigger = function(properties) {
 
     
     // Private values
-    var controller = properties.controller || Controller.Standard.LT;
-    var controllerClick = properties.controllerClick || Controller.Standard.LTClick;
+    var controller = properties.controller || controllerStandard.LT;
+    var controllerClick = properties.controllerClick || controllerStandard.LTClick;
     that.mapping =  Controller.newMapping('com.highfidelity.controller.trigger.' + controller + '-' + controllerClick + '.' + that.label + Math.random());
     Script.scriptEnding.connect(that.mapping.disable);
 
diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js
index 53d4682c97..3e5f487695 100644
--- a/scripts/system/libraries/WebTablet.js
+++ b/scripts/system/libraries/WebTablet.js
@@ -51,7 +51,8 @@ var SUBMESH = 2;
 function calcSpawnInfo(hand, landscape) {
     var finalPosition;
 
-    var LEFT_HAND = Controller.Standard.LeftHand;
+    var controllerStandard = Controller.Standard;
+    var LEFT_HAND = controllerStandard.LeftHand;
     var sensorToWorldScale = MyAvatar.sensorToWorldScale;
     var headPos = (HMD.active && (Camera.mode === "first person" || Camera.mode === "first person look at")) ? HMD.position : Camera.position;
     var headRot = Quat.cancelOutRollAndPitch((HMD.active && (Camera.mode === "first person" || Camera.mode === "first person look at")) ?
diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js
index 83d6fd747b..f331c147e0 100644
--- a/scripts/system/libraries/controllerDispatcherUtils.js
+++ b/scripts/system/libraries/controllerDispatcherUtils.js
@@ -73,6 +73,8 @@
    handsAreTracked: true
 */
 
+var controllerStandard = Controller.Standard;
+
 var MSECS_PER_SEC = 1000.0;
 var INCHES_TO_METERS = 1.0 / 39.3701;
 
@@ -610,8 +612,8 @@ var worldPositionToRegistrationFrameMatrix = function(wptrProps, pos) {
 };
 
 var handsAreTracked = function () {
-    return Controller.getPoseValue(Controller.Standard.LeftHandIndex3).valid ||
-        Controller.getPoseValue(Controller.Standard.RightHandIndex3).valid;
+    return Controller.getPoseValue(controllerStandard.LeftHandIndex3).valid ||
+        Controller.getPoseValue(controllerStandard.RightHandIndex3).valid;
 };
 
 if (typeof module !== 'undefined') {
diff --git a/scripts/system/libraries/controllers.js b/scripts/system/libraries/controllers.js
index 5722458990..eff0bfcbe9 100644
--- a/scripts/system/libraries/controllers.js
+++ b/scripts/system/libraries/controllers.js
@@ -16,6 +16,8 @@
    getControllerWorldLocation:true
  */
 
+var controllerStandard = Controller.Standard;
+
 const GRAB_COMMUNICATIONS_SETTING = "io.highfidelity.isFarGrabbing";
 const setGrabCommunications = function setFarGrabCommunications(on) {
     Settings.setValue(GRAB_COMMUNICATIONS_SETTING, on ? "on" : "");
@@ -29,7 +31,7 @@ const getGrabCommunications = function getFarGrabCommunications() {
 const getGrabPointSphereOffset = function(handController, ignoreSensorToWorldScale) {
     var GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 };  // x = upward, y = forward, z = lateral
     var offset = GRAB_POINT_SPHERE_OFFSET;
-    if (handController === Controller.Standard.LeftHand) {
+    if (handController === controllerStandard.LeftHand) {
         offset = {
             x: -GRAB_POINT_SPHERE_OFFSET.x,
             y: GRAB_POINT_SPHERE_OFFSET.y,
@@ -54,7 +56,7 @@ const getControllerWorldLocation = function (handController, doOffset) {
         valid = pose.valid;
         var controllerJointIndex;
         if (pose.valid) {
-            if (handController === Controller.Standard.RightHand) {
+            if (handController === controllerStandard.RightHand) {
                 controllerJointIndex = MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND");
             } else {
                 controllerJointIndex = MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_LEFTHAND");
diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js
index cecc17e705..572c6a764e 100644
--- a/scripts/system/makeUserConnection.js
+++ b/scripts/system/makeUserConnection.js
@@ -16,6 +16,8 @@
 
     var request = Script.require('request').request;
 
+    var controllerStandard = Controller.Standard;
+
     var WANT_DEBUG = Settings.getValue('MAKE_USER_CONNECTION_DEBUG', false);
     var LABEL = "makeUserConnection";
     var MAX_AVATAR_DISTANCE = 0.2; // m
@@ -150,10 +152,10 @@
     }
 
     function handToString(hand) {
-        if (hand === Controller.Standard.RightHand) {
+        if (hand === controllerStandard.RightHand) {
             return "RightHand";
         }
-        if (hand === Controller.Standard.LeftHand) {
+        if (hand === controllerStandard.LeftHand) {
             return "LeftHand";
         }
         debug("handToString called without valid hand! value: ", hand);
@@ -161,10 +163,10 @@
     }
 
     function handToHaptic(hand) {
-        if (hand === Controller.Standard.RightHand) {
+        if (hand === controllerStandard.RightHand) {
             return 1;
         }
-        if (hand === Controller.Standard.LeftHand) {
+        if (hand === controllerStandard.LeftHand) {
             return 0;
         }
         debug("handToHaptic called without a valid hand!");
@@ -917,25 +919,25 @@
     function keyPressEvent(event) {
         if ((event.text.toUpperCase() === "X") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl
                 && !event.isAlt) {
-            updateTriggers(1.0, true, Controller.Standard.RightHand);
+            updateTriggers(1.0, true, controllerStandard.RightHand);
         }
     }
     function keyReleaseEvent(event) {
         if (event.text.toUpperCase() === "X" && !event.isAutoRepeat) {
-            updateTriggers(0.0, true, Controller.Standard.RightHand);
+            updateTriggers(0.0, true, controllerStandard.RightHand);
         }
     }
     // map controller actions
     var connectionMapping = Controller.newMapping(Script.resolvePath('') + '-grip');
-    connectionMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripHandler(Controller.Standard.LeftHand));
-    connectionMapping.from(Controller.Standard.RightGrip).peek().to(makeGripHandler(Controller.Standard.RightHand));
+    connectionMapping.from(controllerStandard.LeftGrip).peek().to(makeGripHandler(controllerStandard.LeftHand));
+    connectionMapping.from(controllerStandard.RightGrip).peek().to(makeGripHandler(controllerStandard.RightHand));
 
     // setup keyboard initiation
     Controller.keyPressEvent.connect(keyPressEvent);
     Controller.keyReleaseEvent.connect(keyReleaseEvent);
 
     // Xbox controller because that is important
-    connectionMapping.from(Controller.Standard.RB).peek().to(makeGripHandler(Controller.Standard.RightHand, true));
+    connectionMapping.from(controllerStandard.RB).peek().to(makeGripHandler(controllerStandard.RightHand, true));
 
     // it is easy to forget this and waste a lot of time for nothing
     connectionMapping.enable();
diff --git a/scripts/system/miniTablet.js b/scripts/system/miniTablet.js
index 6271276584..acd0d96e9b 100644
--- a/scripts/system/miniTablet.js
+++ b/scripts/system/miniTablet.js
@@ -19,6 +19,8 @@
     Script.include("./libraries/utils.js");
     Script.include("./libraries/controllerDispatcherUtils.js");
 
+    var controllerStandard = Controller.Standard;
+
     var UI,
         ui = null,
         State,
@@ -647,15 +649,15 @@
                 now;
 
             // Shouldn't show mini tablet if hand isn't being controlled.
-            pose = Controller.getPoseValue(hand === LEFT_HAND ? Controller.Standard.LeftHand : Controller.Standard.RightHand);
+            pose = Controller.getPoseValue(hand === LEFT_HAND ? controllerStandard.LeftHand : controllerStandard.RightHand);
             show = pose.valid;
 
             // Shouldn't show mini tablet on hand if that hand's trigger or grip are pressed (i.e., laser is searching or hand 
             // is grabbing something) or the other hand's trigger is pressed unless it is pointing at the mini tablet. Allow 
             // the triggers to be pressed briefly to allow for the grabbing process.
             if (show) {
-                isLeftTriggerOff = Controller.getValue(Controller.Standard.LT) < TRIGGER_OFF_VALUE &&
-                    Controller.getValue(Controller.Standard.LeftGrip) < TRIGGER_OFF_VALUE;
+                isLeftTriggerOff = Controller.getValue(controllerStandard.LT) < TRIGGER_OFF_VALUE &&
+                    Controller.getValue(controllerStandard.LeftGrip) < TRIGGER_OFF_VALUE;
                 if (!isLeftTriggerOff) {
                     if (leftTriggerOn === 0) {
                         leftTriggerOn = Date.now();
@@ -665,8 +667,8 @@
                 } else {
                     leftTriggerOn = 0;
                 }
-                isRightTriggerOff = Controller.getValue(Controller.Standard.RT) < TRIGGER_OFF_VALUE &&
-                    Controller.getValue(Controller.Standard.RightGrip) < TRIGGER_OFF_VALUE;
+                isRightTriggerOff = Controller.getValue(controllerStandard.RT) < TRIGGER_OFF_VALUE &&
+                    Controller.getValue(controllerStandard.RightGrip) < TRIGGER_OFF_VALUE;
                 if (!isRightTriggerOff) {
                     if (rightTriggerOn === 0) {
                         rightTriggerOn = Date.now();
diff --git a/scripts/system/mod.js b/scripts/system/mod.js
index 71d2c32bac..8d443cf8c3 100644
--- a/scripts/system/mod.js
+++ b/scripts/system/mod.js
@@ -16,6 +16,8 @@
 
 Script.include("/~/system/libraries/controllers.js");
 
+var controllerStandard = Controller.Standard;
+
 // grab the toolbar
 var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
 
@@ -236,8 +238,8 @@ function makeClickHandler(hand) {
         }
     };
 }
-triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand));
-triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand));
+triggerMapping.from(controllerStandard.RTClick).peek().to(makeClickHandler(controllerStandard.RightHand));
+triggerMapping.from(controllerStandard.LTClick).peek().to(makeClickHandler(controllerStandard.LeftHand));
 
 triggerMapping.enable();
 
diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js
index 34795ab487..fec9fb2341 100644
--- a/scripts/system/notifications.js
+++ b/scripts/system/notifications.js
@@ -4,7 +4,7 @@
 //
 //  Created by Adrian McCarlie on October 8th, 2014
 //  Copyright 2014 High Fidelity, Inc.
-//  Copyright 2022-2023 Overte e.V.
+//  Copyright 2022-2024 Overte e.V.
 //
 //  Display notifications to the user for some specific events.
 //
@@ -18,6 +18,8 @@
         "create/audioFeedback/audioFeedback.js"
     ]);
 
+    var controllerStandard = Controller.Standard;
+
     var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications";
     var SETTING_ACTIVATION_SNAPSHOT_NOTIFICATIONS = "snapshotNotifications";
     var NOTIFICATION_LIFE_DURATION = 10000; //10 seconds (in millisecond) before expiration.
@@ -33,8 +35,7 @@
 
     //DESKTOP OVERLAY PROPERTIES
     var overlayWidth = 340.0; //width in pixel of notification overlay in desktop
-    var windowDimensions = Controller.getViewportDimensions(); // get the size of the interface window
-    var overlayLocationX = (windowDimensions.x - (overlayWidth + 20.0)); // positions window 20px from the right of the interface window
+    var overlayLocationX = (Window.innerWidth - (overlayWidth + 20.0)); // positions window 20px from the right of the interface window
     var overlayLocationY = 20.0; // position down from top of interface window
     var overlayTopMargin = 13.0;
     var overlayLeftMargin = 10.0;
@@ -44,9 +45,10 @@
     
     //HMD NOTIFICATION PANEL PROPERTIES
     var HMD_UI_SCALE_FACTOR = 1.0; //This define the size of all the notification system in HMD.
-    var hmdPanelLocalPosition = {"x": 1.2, "y": 2, "z": -1.0};
-    var hmdPanelLocalRotation = Quat.fromVec3Degrees({"x": 0, "y": -15, "z": 0});
+    var hmdPanelLocalPosition = {"x": 0.3, "y": 0.25, "z": -1.5};
+    var hmdPanelLocalRotation = Quat.fromVec3Degrees({"x": 0, "y": -3, "z": 0});
     var mainHMDnotificationContainerID = Uuid.NULL;
+    var CAMERA_MATRIX_INDEX = -7;
     
     //HMD LOCAL ENTITY PROPERTIES
     var entityWidth = 0.8; //in meter
@@ -71,8 +73,8 @@
     }
 
     function checkHands() {
-        var myLeftHand = Controller.getPoseValue(Controller.Standard.LeftHand);
-        var myRightHand = Controller.getPoseValue(Controller.Standard.RightHand);
+        var myLeftHand = Controller.getPoseValue(controllerStandard.LeftHand);
+        var myRightHand = Controller.getPoseValue(controllerStandard.RightHand);
         var eyesPosition = MyAvatar.getEyePosition();
         var hipsPosition = MyAvatar.getJointPosition("Hips");
         var eyesRelativeHeight = eyesPosition.y - hipsPosition.y;
@@ -85,6 +87,7 @@
 
     //DISPLAY
     function renderNotifications(remainingTime) {
+        overlayLocationX = (Window.innerWidth - (overlayWidth + 20.0)); 
         var alpha = NOTIFICATION_ALPHA;
         if (remainingTime < FADE_OUT_DURATION) {
             alpha = NOTIFICATION_ALPHA * (remainingTime/FADE_OUT_DURATION);
@@ -188,7 +191,7 @@
                             "subImage": { "x": 0, "y": 0 },
                             "visible": true,
                             "alpha": alpha
-                        };                        
+                        };
                         if (notifications[i].imageOverlayID === Uuid.NULL){
                             properties.imageURL = notifications[i].dataImage.path;
                             notifications[i].imageOverlayID = Overlays.addOverlay("image", properties);
@@ -237,7 +240,7 @@
                 "visible": false,
                 "dimensions": {"x": 0.1, "y": 0.1, "z":0.1},
                 "parentID": MyAvatar.sessionUUID,
-                "parentJointIndex": -2,
+                "parentJointIndex": CAMERA_MATRIX_INDEX,
                 "localPosition": hmdPanelLocalPosition,
                 "localRotation": hmdPanelLocalRotation
             };
diff --git a/scripts/system/pal.js b/scripts/system/pal.js
index 0e4038cb01..743df2ef63 100644
--- a/scripts/system/pal.js
+++ b/scripts/system/pal.js
@@ -18,6 +18,7 @@
 //
 
 (function () { // BEGIN LOCAL_SCOPE
+var controllerStandard = Controller.Standard;
 
 var request = Script.require('request').request;
 var AppUi = Script.require('appUi');
@@ -715,10 +716,10 @@ function makePressHandler(hand) {
         handleTriggerPressed(hand, value);
     };
 }
-triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand));
-triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand));
-triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand));
-triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand));
+triggerMapping.from(controllerStandard.RTClick).peek().to(makeClickHandler(controllerStandard.RightHand));
+triggerMapping.from(controllerStandard.LTClick).peek().to(makeClickHandler(controllerStandard.LeftHand));
+triggerPressMapping.from(controllerStandard.RT).peek().to(makePressHandler(controllerStandard.RightHand));
+triggerPressMapping.from(controllerStandard.LT).peek().to(makePressHandler(controllerStandard.LeftHand));
 
 var ui;
 // Most apps can have people toggle the tablet closed and open again, and the app should remain "open" even while
diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js
index 1957ecea4d..e41b2a62a8 100644
--- a/scripts/system/tablet-ui/tabletUI.js
+++ b/scripts/system/tablet-ui/tabletUI.js
@@ -17,6 +17,8 @@
    MyAvatar, Menu, AvatarInputs, Vec3, cleanUpOldMaterialEntities */
 
 (function() { // BEGIN LOCAL_SCOPE
+    var controllerStandard = Controller.Standard;
+
     var tabletRezzed = false;
     var activeHand = null;
     var DEFAULT_WIDTH = 0.4375;
@@ -292,27 +294,27 @@
     var clickMapping = Controller.newMapping('tabletToggle-click');
     var wantsMenu = 0;
     clickMapping.from(function () { return wantsMenu; }).to(Controller.Actions.ContextMenu);
-    clickMapping.from(Controller.Standard.RightSecondaryThumb).peek().when(Controller.Hardware.Application.LeftHandDominant).to(function (clicked) {
+    clickMapping.from(controllerStandard.RightSecondaryThumb).peek().when(Controller.Hardware.Application.LeftHandDominant).to(function (clicked) {
     if (clicked) {
-        //activeHudPoint2d(Controller.Standard.RightHand);
-        Messages.sendLocalMessage("toggleHand", Controller.Standard.RightHand);
+        //activeHudPoint2d(controllerStandard.RightHand);
+        Messages.sendLocalMessage("toggleHand", controllerStandard.RightHand);
     }
         wantsMenu = clicked;
     });
     
-    clickMapping.from(Controller.Standard.LeftSecondaryThumb).peek().when(Controller.Hardware.Application.RightHandDominant).to(function (clicked) {
+    clickMapping.from(controllerStandard.LeftSecondaryThumb).peek().when(Controller.Hardware.Application.RightHandDominant).to(function (clicked) {
         if (clicked) {
-            //activeHudPoint2d(Controller.Standard.LeftHand);
-            Messages.sendLocalMessage("toggleHand", Controller.Standard.LeftHand);
+            //activeHudPoint2d(controllerStandard.LeftHand);
+            Messages.sendLocalMessage("toggleHand", controllerStandard.LeftHand);
         }
         wantsMenu = clicked;
     });
 
-    clickMapping.from(Controller.Standard.Start).peek().to(function (clicked) {
+    clickMapping.from(controllerStandard.Start).peek().to(function (clicked) {
     if (clicked) {
         //activeHudPoint2dGamePad();
         var noHands = -1;
-        Messages.sendLocalMessage("toggleHand", Controller.Standard.LeftHand);
+        Messages.sendLocalMessage("toggleHand", controllerStandard.LeftHand);
     }
 
         wantsMenu = clicked;
diff --git a/tests/model-serializers/CMakeLists.txt b/tests/model-serializers/CMakeLists.txt
index 1072a87052..b951f11ead 100644
--- a/tests/model-serializers/CMakeLists.txt
+++ b/tests/model-serializers/CMakeLists.txt
@@ -77,7 +77,7 @@ macro (setup_testcase_dependencies)
       gltf_samples
       PREFIX "models"
       GIT_REPOSITORY "https://github.com/KhronosGroup/glTF-Sample-models"
-      GIT_TAG "master"
+      GIT_TAG "main"
       DOWNLOAD_NO_EXTRACT true CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND ""
     )
 
diff --git a/tests/model-serializers/src/ModelSerializersTests.cpp b/tests/model-serializers/src/ModelSerializersTests.cpp
index 6988b3761e..05e3633d7a 100644
--- a/tests/model-serializers/src/ModelSerializersTests.cpp
+++ b/tests/model-serializers/src/ModelSerializersTests.cpp
@@ -74,17 +74,17 @@ void ModelSerializersTests::loadGLTF_data() {
     QTest::newRow("ready-player-me-good3")   << "models/src/Franny.glb.gz"                     << false << false << false;
     QTest::newRow("ready-player-me-good4")   << "models/src/womanInTShirt.glb.gz"              << false << false << false;
     QTest::newRow("ready-player-me-good5")   << "models/src/female-avatar-with-swords.glb.gz"  << false << false << false;
-    QTest::newRow("ready-player-me-broken1") << "models/src/broken-2022-11-27.glb.gz" << false << true;
+    QTest::newRow("ready-player-me-broken1") << "models/src/broken-2022-11-27.glb.gz" << false << false << false;
 
 
     // We can't parse GLTF 1.0 at present, and probably not ever. We're expecting all these to fail.
-    QDirIterator it("models/src/gltf_samples/1.0", QStringList() << "*.glb", QDir::Files, QDirIterator::Subdirectories);
+    /*QDirIterator it("models/src/gltf_samples/1.0", QStringList() << "*.glb", QDir::Files, QDirIterator::Subdirectories);
     while(it.hasNext()) {
         QString filename = it.next();
         QFileInfo fi(filename);
         QString testname = "gltf1.0-" + fi.fileName();
         QTest::newRow(testname.toUtf8().data()) << filename << true << false << false;
-    }
+    }*/
 
     QDirIterator it2("models/src/gltf_samples/2.0", QStringList() << "*.glb", QDir::Files, QDirIterator::Subdirectories);
     while(it2.hasNext()) {
diff --git a/tests/networking/src/PacketTests.cpp b/tests/networking/src/PacketTests.cpp
index 1b13a38488..acd7dd6e73 100644
--- a/tests/networking/src/PacketTests.cpp
+++ b/tests/networking/src/PacketTests.cpp
@@ -92,6 +92,9 @@ void PacketTests::readTest() {
 }
 
 void PacketTests::writePastCapacityTest() {
+    #ifndef QT_NO_DEBUG
+    QSKIP("This test triggers an assertion when built in debug mode");
+    #else
     auto packet = NLPacket::create(PacketType::Unknown);
 
     auto size = packet->getPayloadCapacity();
@@ -116,6 +119,7 @@ void PacketTests::writePastCapacityTest() {
 
     // NLPacket::write() shouldn't allow the caller to write if no space is left
     QCOMPARE(packet->getPayloadSize(), size);
+    #endif
 }
 
 void PacketTests::primitiveTest() {
diff --git a/tests/networking/src/QtNetworkTests.cpp b/tests/networking/src/QtNetworkTests.cpp
new file mode 100644
index 0000000000..e266ff216c
--- /dev/null
+++ b/tests/networking/src/QtNetworkTests.cpp
@@ -0,0 +1,167 @@
+//
+//  PacketTests.cpp
+//  tests/networking/src
+//
+//  Created by Dale Glass on 02/06/2024
+//  Copyright 2024 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
+//
+
+#include "QtNetworkTests.h"
+#include <test-utils/QTestExtensions.h>
+
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QUrl>
+#include <QHostInfo>
+#include <QTcpSocket>
+#include <QSslSocket>
+#include <QSslCipher>
+
+/**
+ * @brief Test basic Qt networking functionality
+ *
+ * This test was created to test a problem found with the Conan PR.
+ * Possibly some sort of library trouble.
+ *
+ * Normally there's no reason why this should go wrong, so it's mostly
+ * a test of that Qt is deployed and working properly.
+ *
+ * Turns out it's some sort of initialization issue. It can be reproduced by calling the test as:
+ *    ./networking-QtNetworkTests httpsRequestNoSSLVersion
+ *
+ * This test may get stuck on some systems. Running the full suite, or:
+ *    ./networking-QtNetworkTests httpsRequestNoSSLVersion
+ *
+ * will work correctly because  QSslSocket::sslLibraryVersionString() initializes something.
+ */
+const QUrl HTTP_URL("http://ping.archlinux.org/");
+const QUrl HTTPS_URL("https://ping.archlinux.org/");
+const QString TCP_HOST("ping.archlinux.org");
+const QString SSL_HOST("ping.archlinux.org");
+
+
+
+QTEST_MAIN(QtNetworkTests);
+
+void QtNetworkTests::initTestCase() {
+    qDebug() << "Init";
+    qRegisterMetaType<QNetworkReply*>();
+
+}
+
+void QtNetworkTests::tcpSocket() {
+    QTcpSocket sock;
+    QSignalSpy spy(&sock, &QTcpSocket::connected);
+
+    qDebug() << "Connecting to" << TCP_HOST << "on port 80";
+    sock.connectToHost(TCP_HOST, 80);
+    spy.wait();
+    QVERIFY(sock.waitForConnected());
+    QVERIFY(sock.localPort() > 0);
+
+    qDebug() << "Local address is" << sock.localAddress()  << ":" << sock.localPort();
+}
+
+void QtNetworkTests::sslSocket() {
+    QSslSocket sock;
+    QSignalSpy spy(&sock, &QSslSocket::connected);
+
+    QVERIFY(QSslSocket::supportsSsl());
+    qDebug() << "SSL library version: " << QSslSocket::sslLibraryVersionString();
+
+    qDebug() << "Connecting to" << SSL_HOST << "on port 443";
+    sock.connectToHostEncrypted(SSL_HOST, 443);
+    spy.wait();
+    QVERIFY(sock.waitForEncrypted());
+    QVERIFY(sock.localPort() > 0);
+
+    QVERIFY(!sock.sslConfiguration().isNull());
+    QVERIFY(sock.sslHandshakeErrors().length() == 0);
+    QVERIFY(sock.sessionProtocol() != QSsl::UnknownProtocol);
+
+    qDebug() << "Local address is" << sock.localAddress()  << ":" << sock.localPort();
+    qDebug() << "SSL protocol : " << sock.sessionProtocol();
+    qDebug() << "SSL cipher   : " << sock.sessionCipher().protocolString();
+    qDebug() << "SSL cert     : " << sock.peerCertificate();
+}
+
+
+void QtNetworkTests::httpRequest() {
+    auto manager = new QNetworkAccessManager();
+
+    qDebug() << "Making request to" << HTTP_URL;
+    QSignalSpy spy(manager, &QNetworkAccessManager::finished);
+    QNetworkRequest req(HTTP_URL);
+    manager->get(req);
+
+    spy.wait();
+
+    QCOMPARE(spy.count(), 1);
+    QList<QVariant> arguments = spy.takeFirst();
+    QNetworkReply *reply = arguments.at(0).value<QNetworkReply*>();
+    QVERIFY(!reply->error());
+    QVERIFY(!reply->sslConfiguration().isNull());
+
+    QString data = reply->readAll();
+    qDebug() << "DATA: " << data;
+}
+
+
+// Unlike the test below this works, because the sslLibraryVersionString call pokes something
+// in the Qt guts to make things initialize properly.
+
+void QtNetworkTests::httpsRequest() {
+    auto manager = new QNetworkAccessManager();
+
+    qDebug() << "SSL library version      : " << QSslSocket::sslLibraryVersionString();
+    qDebug() << "SSL library version      : " << QSslSocket::sslLibraryVersionString();
+    qDebug() << "SSL library build version: " << QSslSocket::sslLibraryBuildVersionString();
+
+
+    qDebug() << "Making request to" << HTTPS_URL;
+    QSignalSpy spy(manager, &QNetworkAccessManager::finished);
+    QNetworkRequest req(HTTPS_URL);
+    manager->get(req);
+
+    spy.wait();
+
+    QCOMPARE(spy.count(), 1);
+    QList<QVariant> arguments = spy.takeFirst();
+    QNetworkReply *reply = arguments.at(0).value<QNetworkReply*>();
+    QVERIFY(!reply->error());
+    QVERIFY(!reply->sslConfiguration().isNull());
+    qDebug() << "Peer cert:" << reply->sslConfiguration().peerCertificate();
+    QString data = reply->readAll();
+    qDebug() << "DATA: " << data;
+    qDebug() << "SSL library version: " << QSslSocket::sslLibraryVersionString();
+
+}
+
+
+// On some systems, this hangs forever. Something in the Qt guts fails to initialize.
+
+void QtNetworkTests::httpsRequestNoSSLVersion() {
+    auto manager = new QNetworkAccessManager();
+
+    qDebug() << "Making request to" << HTTPS_URL;
+    QSignalSpy spy(manager, &QNetworkAccessManager::finished);
+    QNetworkRequest req(HTTPS_URL);
+    manager->get(req);
+
+    spy.wait();
+
+    QCOMPARE(spy.count(), 1);
+    QList<QVariant> arguments = spy.takeFirst();
+    QNetworkReply *reply = arguments.at(0).value<QNetworkReply*>();
+    QVERIFY(!reply->error());
+    QVERIFY(!reply->sslConfiguration().isNull());
+    qDebug() << "Peer cert:" << reply->sslConfiguration().peerCertificate();
+    QString data = reply->readAll();
+    qDebug() << "DATA: " << data;
+    qDebug() << "SSL library version: " << QSslSocket::sslLibraryVersionString();
+
+}
diff --git a/tests/networking/src/QtNetworkTests.h b/tests/networking/src/QtNetworkTests.h
new file mode 100644
index 0000000000..1744598570
--- /dev/null
+++ b/tests/networking/src/QtNetworkTests.h
@@ -0,0 +1,31 @@
+//
+//  PacketTests.cpp
+//  tests/networking/src
+//
+//  Created by Dale Glass on 02/06/2024
+//  Copyright 2024 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
+//
+
+
+#ifndef overte_QtNetworkTests_h
+#define overte_QtNetworkTests_h
+
+#pragma once
+
+#include <QtTest/QtTest>
+
+class QtNetworkTests : public QObject {
+    Q_OBJECT
+private slots:
+    void initTestCase();
+    void tcpSocket();
+    void sslSocket();
+    void httpRequest();
+    void httpsRequestNoSSLVersion();
+    void httpsRequest();
+};
+
+#endif // overte_QtNetworkTests_h
diff --git a/tests/networking/src/ResourceTests.cpp b/tests/networking/src/ResourceTests.cpp
index 770fa77a07..18c55d3809 100644
--- a/tests/networking/src/ResourceTests.cpp
+++ b/tests/networking/src/ResourceTests.cpp
@@ -17,6 +17,7 @@
 #include <NodeList.h>
 #include <NetworkAccessManager.h>
 #include <DependencyManager.h>
+#include <ResourceRequestObserver.h>
 #include <StatTracker.h>
 
 QTEST_MAIN(ResourceTests)
@@ -29,6 +30,8 @@ void ResourceTests::initTestCase() {
     DependencyManager::set<NodeList>(NodeType::Agent, INVALID_PORT);
     DependencyManager::set<ResourceCacheSharedItems>();
     DependencyManager::set<ResourceManager>();
+    DependencyManager::set<ResourceRequestObserver>();
+
     const qint64 MAXIMUM_CACHE_SIZE = 1024 * 1024 * 1024; // 1GB
 
     // set up the file cache
diff --git a/tests/script-engine/src/ScriptEngineBenchmarkTests.cpp b/tests/script-engine/src/ScriptEngineBenchmarkTests.cpp
new file mode 100644
index 0000000000..150e1a28cb
--- /dev/null
+++ b/tests/script-engine/src/ScriptEngineBenchmarkTests.cpp
@@ -0,0 +1,200 @@
+//
+//  Copyright 2023 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
+//  SPDX-License-Identifier: Apache-2.0
+//
+
+#include <QSignalSpy>
+#include <QDebug>
+#include <QFile>
+#include <QTextStream>
+
+
+#include "ScriptEngineBenchmarkTests.h"
+#include "DependencyManager.h"
+
+#include "ScriptEngines.h"
+#include "ScriptEngine.h"
+#include "ScriptCache.h"
+#include "ScriptManager.h"
+
+#include "v8/ScriptObjectV8Proxy.h"
+#include "v8/ScriptEngineV8.h"
+
+#include "ResourceManager.h"
+#include "ResourceRequestObserver.h"
+#include "StatTracker.h"
+
+#include "NodeList.h"
+#include "../../../libraries/entities/src/EntityScriptingInterface.h"
+
+QTEST_MAIN(ScriptEngineBenchmarkTests)
+
+
+
+
+
+
+void ScriptEngineBenchmarkTests::initTestCase() {
+    // AudioClient starts networking, but for the purposes of the tests here we don't care,
+    // so just got to use some port.
+    //int listenPort = 10000;
+
+    //DependencyManager::registerInheritance<LimitedNodeList, NodeList>();
+    //DependencyManager::set<NodeList>(NodeType::Agent, listenPort);
+    DependencyManager::set<ScriptEngines>(ScriptManager::NETWORKLESS_TEST_SCRIPT, QUrl(""));
+    DependencyManager::set<ScriptCache>();
+   // DependencyManager::set<ResourceManager>();
+   // DependencyManager::set<ResourceRequestObserver>();
+    DependencyManager::set<StatTracker>();
+    DependencyManager::set<ScriptInitializers>();
+   // DependencyManager::set<EntityScriptingInterface>(true);
+
+
+}
+
+ScriptManagerPointer ScriptEngineBenchmarkTests::makeManager(const QString &scriptSource, const QString &scriptFilename) {
+    ScriptManagerPointer sm = newScriptManager(ScriptManager::NETWORKLESS_TEST_SCRIPT, scriptSource, scriptFilename);
+
+
+    sm->setAbortOnUncaughtException(true);
+
+    connect(sm.get(), &ScriptManager::scriptLoaded, [](const QString& filename){
+        qWarning() << "Loaded script" << filename;
+    });
+
+
+    connect(sm.get(), &ScriptManager::errorLoadingScript, [](const QString& filename){
+        qWarning() << "Failed to load script" << filename;
+    });
+
+    connect(sm.get(), &ScriptManager::printedMessage, [](const QString& message, const QString& engineName){
+        qDebug() << "Printed message from engine" << engineName << ": " << message;
+    });
+
+    connect(sm.get(), &ScriptManager::infoMessage, [](const QString& message, const QString& engineName){
+        qInfo() << "Info message from engine" << engineName << ": " << message;
+    });
+
+    connect(sm.get(), &ScriptManager::warningMessage, [](const QString& message, const QString& engineName){
+        qWarning() << "Warning from engine" << engineName << ": " << message;
+    });
+
+    connect(sm.get(), &ScriptManager::errorMessage, [](const QString& message, const QString& engineName){
+        qCritical() << "Error from engine" << engineName << ": " << message;
+    });
+
+    connect(sm.get(), &ScriptManager::finished, [](const QString& fileNameString, ScriptManagerPointer smp){
+        qInfo() << "Finished running script" << fileNameString;
+    });
+
+    connect(sm.get(), &ScriptManager::runningStateChanged, [sm](){
+        qInfo() << "Running state changed. Running = " << sm->isRunning() << "; Stopped = " << sm->isStopped() << "; Finished = " << sm->isFinished();
+    });
+
+    connect(sm.get(), &ScriptManager::unhandledException, [](std::shared_ptr<ScriptException> exception){
+        qWarning() << "Exception from engine: " << exception;
+    });
+
+
+    return sm;
+}
+
+void ScriptEngineBenchmarkTests::benchmarkSetProperty() {
+    auto sm = makeManager("print(\"script works!\"); Script.stop(true);", "testTrivial.js");
+
+    auto engine = sm->engine();
+    auto obj = engine->newObject();
+
+    QBENCHMARK {
+        engine->setProperty("hello", QVariant(1));
+    }
+
+
+}
+
+void ScriptEngineBenchmarkTests::benchmarkSetProperty1K() {
+    auto sm = makeManager("print(\"script works!\"); Script.stop(true);", "testTrivial.js");
+
+    auto engine = sm->engine();
+    auto obj = engine->newObject();
+
+    int i = 0;
+    char buf[128];
+
+    QBENCHMARK {
+        for(i=0;i<1024;i++) {
+            sprintf(buf, "%i", i);
+            engine->setProperty(buf, QVariant(1));
+        }
+    }
+
+
+}
+
+void ScriptEngineBenchmarkTests::benchmarkSetProperty16K() {
+    auto sm = makeManager("print(\"script works!\"); Script.stop(true);", "testTrivial.js");
+
+    auto engine = sm->engine();
+    auto obj = engine->newObject();
+
+    int i = 0;
+    char buf[128];
+
+    QBENCHMARK {
+        for(i=0;i<16384;i++) {
+            sprintf(buf, "%i", i);
+            engine->setProperty(buf, QVariant(1));
+        }
+    }
+
+
+}
+
+
+void ScriptEngineBenchmarkTests::benchmarkQueryProperty() {
+    auto sm = makeManager("print(\"script works!\"); Script.stop(true);", "testTrivial.js");
+
+    auto engine = sm->engine();
+    auto obj = engine->newObject();
+
+
+
+    int i = 0;
+    char buf[128];
+
+    QObject dummy;
+
+    ScriptEngineV8 *v8_engine = dynamic_cast<ScriptEngineV8*>(engine.get());
+
+    ScriptObjectV8Proxy proxy(v8_engine, &dummy, false, ScriptEngine::QObjectWrapOptions());
+
+    for(i=0;i<16384;i++) {
+        sprintf(buf, "%i", i);
+        engine->setProperty(buf, QVariant(1));
+    }
+
+    QSKIP("Test not implemented yet");
+
+//    QBENCHMARK {
+//        engine->property()
+//    }
+
+}
+
+
+void ScriptEngineBenchmarkTests::benchmarkSimpleScript() {
+
+    QBENCHMARK {
+        auto sm = makeManager("print(\"script works!\"); Script.stop(true);", "testTrivial.js");
+        QString printed;
+        connect(sm.get(), &ScriptManager::printedMessage, [&printed](const QString& message, const QString& engineName){
+            printed.append(message);
+        });
+
+        sm->run();
+    }
+
+}
diff --git a/tests/script-engine/src/ScriptEngineBenchmarkTests.h b/tests/script-engine/src/ScriptEngineBenchmarkTests.h
new file mode 100644
index 0000000000..b8884a8101
--- /dev/null
+++ b/tests/script-engine/src/ScriptEngineBenchmarkTests.h
@@ -0,0 +1,36 @@
+//
+//  SciptEngineBenchmarks.h
+//  tests/script-engine/src
+//
+//  Created by Dale Glass
+//  Copyright 2023 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
+//  SPDX-License-Identifier: Apache-2.0
+//
+
+#pragma once
+
+#include <QtTest/QtTest>
+#include "ScriptManager.h"
+#include "ScriptEngine.h"
+
+
+using ScriptManagerPointer = std::shared_ptr<ScriptManager>;
+
+
+class ScriptEngineBenchmarkTests : public QObject {
+    Q_OBJECT
+private slots:
+    void initTestCase();
+    void benchmarkSetProperty();
+    void benchmarkSetProperty1K();
+    void benchmarkSetProperty16K();
+    void benchmarkQueryProperty();
+    void benchmarkSimpleScript();
+
+private:
+    ScriptManagerPointer makeManager(const QString &source, const QString &filename);
+};
+
diff --git a/tests/script-engine/src/ScriptEngineNetworkedTests.cpp b/tests/script-engine/src/ScriptEngineNetworkedTests.cpp
index 9648e88d56..c757e009c6 100644
--- a/tests/script-engine/src/ScriptEngineNetworkedTests.cpp
+++ b/tests/script-engine/src/ScriptEngineNetworkedTests.cpp
@@ -120,7 +120,7 @@ ScriptManagerPointer ScriptEngineNetworkedTests::makeManager(const QString &scri
     return sm;
 }
 
-void ScriptEngineNetworkedTests::testRequire() {
+void ScriptEngineNetworkedTests::testScriptRequire() {
     auto sm = makeManager(
         "print(\"Starting\");"
         "Script.require('./tests/c.js');"
@@ -153,6 +153,38 @@ void ScriptEngineNetworkedTests::testRequire() {
     }
 }
 
+void ScriptEngineNetworkedTests::testRequire() {
+    auto sm = makeManager(
+        "print(\"Starting\");"
+        "require('./tests/c_require.js');"
+        "print(\"Done\");"
+        "Script.stop(true);", "testRequire.js");
+    QStringList printed;
+    QStringList expected {"Starting", "Value from A: 6", "Value from B: 6", "Done"};
+
+
+    QVERIFY(!sm->isRunning());
+    QVERIFY(!sm->isStopped());
+    QVERIFY(!sm->isFinished());
+
+    connect(sm.get(), &ScriptManager::printedMessage, [&printed](const QString& message, const QString& engineName){
+        printed.append(message);
+    });
+
+
+    qInfo() << "About to run script";
+    sm->run();
+
+    QVERIFY(!sm->isRunning());
+    QVERIFY(!sm->isStopped());
+    QVERIFY(sm->isFinished());
+
+    QVERIFY(printed.length() == expected.length());
+    for(int i=0;i<printed.length();i++) {
+        QString nomatch = QString("Result '%1' didn't match expected '%2'").arg(printed[i]).arg(expected[i]);
+        QVERIFY2(printed[i] == expected[i], qPrintable(nomatch));
+    }
+}
 
 
 void ScriptEngineNetworkedTests::testRequireInfinite() {
diff --git a/tests/script-engine/src/ScriptEngineNetworkedTests.h b/tests/script-engine/src/ScriptEngineNetworkedTests.h
index d88478f83e..f1b25440f1 100644
--- a/tests/script-engine/src/ScriptEngineNetworkedTests.h
+++ b/tests/script-engine/src/ScriptEngineNetworkedTests.h
@@ -27,6 +27,7 @@ class ScriptEngineNetworkedTests : public QObject {
 private slots:
     void initTestCase();
     void testRequire();
+    void testScriptRequire();
     void testRequireInfinite();
 
 
diff --git a/tests/script-engine/src/tests/b_require.js b/tests/script-engine/src/tests/b_require.js
new file mode 100644
index 0000000000..2022cb641c
--- /dev/null
+++ b/tests/script-engine/src/tests/b_require.js
@@ -0,0 +1,6 @@
+
+// b.js
+var a = require('./a.js');
+a.value += 1;
+console.log('message from b');
+module.exports = a.value;
\ No newline at end of file
diff --git a/tests/script-engine/src/tests/c_require.js b/tests/script-engine/src/tests/c_require.js
new file mode 100644
index 0000000000..ea7b005b53
--- /dev/null
+++ b/tests/script-engine/src/tests/c_require.js
@@ -0,0 +1,6 @@
+
+// c.js
+var a = require('./a.js');
+var b = require('./b_require.js');
+print("Value from A: " + a.value);
+print("Value from B: " + b);
\ No newline at end of file
diff --git a/tests/shared/src/BitVectorHelperTests.cpp b/tests/shared/src/BitVectorHelperTests.cpp
index c99c85d377..8232394c03 100644
--- a/tests/shared/src/BitVectorHelperTests.cpp
+++ b/tests/shared/src/BitVectorHelperTests.cpp
@@ -34,7 +34,7 @@ static void readWriteHelper(const std::vector<bool>& src) {
     int numBits = (int)src.size();
     int numBytes = calcBitVectorSize(numBits);
     uint8_t* bytes = new uint8_t[numBytes];
-    memset(bytes, numBytes, sizeof(uint8_t));
+    memset(bytes, 0, numBytes);
     int numBytesWritten = writeBitVector(bytes, numBits, [&](int i) {
         return src[i];
     });
@@ -53,6 +53,8 @@ static void readWriteHelper(const std::vector<bool>& src) {
         bool b = dst[i];
         QCOMPARE(a, b);
     }
+
+    delete[] bytes;
 }
 
 void BitVectorHelperTests::readWriteTest() {
diff --git a/tests/shared/src/FileCacheTests.cpp b/tests/shared/src/FileCacheTests.cpp
index b7c2103817..d90e01d590 100644
--- a/tests/shared/src/FileCacheTests.cpp
+++ b/tests/shared/src/FileCacheTests.cpp
@@ -132,6 +132,11 @@ void FileCacheTests::testFreeSpacePreservation() {
     cache->setMinFreeSize(targetFreeSpace);
     QCOMPARE(cache->getNumCachedFiles(), (size_t)5);
     QCOMPARE(cache->getNumTotalFiles(), (size_t)5);
+    qDebug() << "Free space: " << getFreeSpace();
+    qDebug() << "Target    : " << targetFreeSpace;
+
+    qInfo() << "The following test may fail if free disk space was changed by another program during the test's runtime";
+
     QVERIFY(getFreeSpace() >= targetFreeSpace);
     for (int i = 0; i < 95; ++i) {
         std::string key = getFileKey(i);
diff --git a/tests/shared/src/MovingPercentileTests.cpp b/tests/shared/src/MovingPercentileTests.cpp
index daf96ca188..c2be130c64 100644
--- a/tests/shared/src/MovingPercentileTests.cpp
+++ b/tests/shared/src/MovingPercentileTests.cpp
@@ -18,6 +18,7 @@
 #include <QtCore/QQueue>
 #include <test-utils/GLMTestUtils.h>
 #include <test-utils/QTestExtensions.h>
+#include <QRandomGenerator64>
 
 QTEST_MAIN(MovingPercentileTests)
 
@@ -41,70 +42,67 @@ void MovingPercentileTests::testRunningMedian() {
 
 
 int64_t MovingPercentileTests::random() {
-    return ((int64_t) rand() << 48) ^
-            ((int64_t) rand() << 32) ^
-            ((int64_t) rand() << 16) ^
-            ((int64_t) rand());
+    return QRandomGenerator64::global()->generate();
 }
 
 void MovingPercentileTests::testRunningMinForN (int n) {
     // Stores the last n samples
     QQueue<int64_t> samples;
-    
+
     MovingPercentile movingMin (n, 0.0f);
-    
+
     for (int s = 0; s < 3 * n; ++s) {
         int64_t sample = random();
-        
+
         samples.push_back(sample);
         if (samples.size() > n)
             samples.pop_front();
-        
+
         if (samples.size() == 0) {
             QFAIL_WITH_MESSAGE("\n\n\n\tWTF\n\tsamples.size() = " << samples.size() << ", n = " << n);
         }
-        
+
         movingMin.updatePercentile(sample);
-        
+
         // Calculate the minimum of the moving samples
         int64_t expectedMin = std::numeric_limits<int64_t>::max();
-        
+
         int prevSize = samples.size();
         for (auto val : samples) {
             expectedMin = std::min(val, expectedMin);
         }
         QCOMPARE(samples.size(), prevSize);
-        
+
         QVERIFY(movingMin.getValueAtPercentile() - expectedMin == 0L);
     }
 }
 
 void MovingPercentileTests::testRunningMaxForN (int n) {
-    
+
     // Stores the last n samples
     QQueue<int64_t> samples;
-    
+
     MovingPercentile movingMax (n, 1.0f);
-    
+
     for (int s = 0; s < 10000; ++s) {
         int64_t sample = random();
-        
+
         samples.push_back(sample);
         if (samples.size() > n) {
             samples.pop_front();
         }
-        
+
         if (samples.size() == 0) {
             QFAIL_WITH_MESSAGE("\n\n\n\tWTF\n\tsamples.size() = " << samples.size() << ", n = " << n);
         }
-        
+
         movingMax.updatePercentile(sample);
-        
+
         // Calculate the maximum of the moving samples
         int64_t expectedMax = std::numeric_limits<int64_t>::min();
         for (auto val : samples)
             expectedMax = std::max(val, expectedMax);
-        
+
         QVERIFY(movingMax.getValueAtPercentile() - expectedMax == 0L);
     }
 }
@@ -112,34 +110,34 @@ void MovingPercentileTests::testRunningMaxForN (int n) {
 void MovingPercentileTests::testRunningMedianForN (int n) {
     // Stores the last n samples
     QQueue<int64_t> samples;
-    
+
     MovingPercentile movingMedian (n, 0.5f);
-    
+
     for (int s = 0; s < 10000; ++s) {
         int64_t sample = random();
-        
+
         samples.push_back(sample);
         if (samples.size() > n)
             samples.pop_front();
-        
+
         if (samples.size() == 0) {
             QFAIL_WITH_MESSAGE("\n\n\n\tWTF\n\tsamples.size() = " << samples.size() << ", n = " << n);
         }
-        
+
         movingMedian.updatePercentile(sample);
         auto median = movingMedian.getValueAtPercentile();
-        
+
         // Check the number of samples that are > or < median
         int samplesGreaterThan = 0;
         int samplesLessThan    = 0;
-        
+
         for (auto value : samples) {
             if (value < median)
                 ++samplesGreaterThan;
             else if (value > median)
                 ++samplesLessThan;
         }
-        
+
         QCOMPARE_WITH_LAMBDA(samplesLessThan, n / 2, [=]() {
             return samplesLessThan <= n / 2;
         });
diff --git a/tools/ci-scripts/build_server_package.bash b/tools/ci-scripts/build_server_package.bash
new file mode 100755
index 0000000000..6eef824c63
--- /dev/null
+++ b/tools/ci-scripts/build_server_package.bash
@@ -0,0 +1,87 @@
+#!/bin/bash
+
+# Copyright 2024 Overte e.V.
+# SPDX-License-Identifier: Apache-2.0
+
+# OS=debian-11 TAG=2024.06.1 ARCH=amd64 ./build_server_package.bash
+# Currently supported OSs debian-11 debian-12 ubuntu-20.04 ubuntu-22.04 ubuntu-24.04 fedora-39 fedora-40 rockylinux-9
+# Remember to add a CMAKE_BACKTRACE_URL below if applicable.
+
+echo "Cloning Overte $TAG …"
+git clone --depth 1 --branch $TAG https://github.com/overte-org/overte.git Overte-$TAG-$OS-$ARCH
+cd Overte-$TAG-$OS-$ARCH
+
+cat > commands.bash <<EOF
+#!/bin/bash
+# Automatically generated
+
+echo "Preparing environment …";
+export CMAKE_BACKTRACE_URL=""
+export CMAKE_BACKTRACE_TOKEN="$TAG"
+export PRODUCTION_BUILD=1
+export RELEASE_NUMBER="$TAG"
+export RPMVERSION="$TAG"
+export DEBVERSION="$TAG-$OS"
+export DEBEMAIL="julian.gro@overte.org"
+export DEBFULLNAME="Julian Groß"
+if [[ "$OS" == "ubuntu-18.04" || "$OS" == "ubuntu-20.04" ]]; then
+    : # Do nothing. Don't set OVERTE_USE_SYSTEM_QT
+else
+    export OVERTE_USE_SYSTEM_QT=true
+fi
+if [[ "$OS" =~ "ubuntu" || "$OS" =~ "debian" ]]; then
+    # Debian
+    apt update
+    apt dist-upgrade -y
+else
+    # RPM
+    dnf upgrade -y
+fi
+
+cd /overte
+rm -rf build && mkdir build
+cd build || exit
+
+echo "Configuring …"
+if [ "$ARCH" == "amd64" ]; then
+    cmake .. -DOVERTE_CPU_ARCHITECTURE=-msse3 -DVCPKG_BUILD_TYPE=release -DSERVER_ONLY=true -DBUILD_TOOLS=true
+else
+    # aarch64
+    VCPKG_FORCE_SYSTEM_BINARIES=1 cmake .. -DOVERTE_CPU_ARCHITECTURE= -DVCPKG_BUILD_TYPE=release -DSERVER_ONLY=true -DBUILD_TOOLS=true
+fi
+
+echo "Building …"
+make domain-server assignment-client oven -j$(nproc) || exit
+
+echo "Packaging for $OS …"
+cd ../pkg-scripts || exit; \
+if [[ "$OS" =~ "ubuntu" || "$OS" =~ "debian" ]]; then
+    # Debian
+    ./make-deb-server || /bin/bash;
+else
+    # RPM
+    ./make-rpm-server || /bin/bash;
+fi
+
+echo "Preparing files for Sentry …"
+cd .. || exit
+tar -cf Senty_upload_$TAG-$OS-$ARCH.tar build || exit
+
+
+
+EOF
+
+if [[ "$OS" =~ "ubuntu" || "$OS" =~ "debian" || "$OS" == "rockylinux-9" ]]; then
+    docker run -v $(pwd):/overte -it overte/overte-server-build:0.1.3-$OS-$ARCH /bin/bash -c " \
+    cd /overte; \
+    chmod +x commands.bash; \
+    ./commands.bash; \
+    "
+else
+    docker run -v $(pwd):/overte -it overte/overte-server-build:0.1.4-$OS-$ARCH /bin/bash -c " \
+    cd /overte; \
+    chmod +x commands.bash; \
+    ./commands.bash; \
+    "
+fi
+
diff --git a/tools/ci-scripts/deb_package/Dockerfile_build_ubuntu-18.04 b/tools/ci-scripts/deb_package/Dockerfile_build_ubuntu-24.04
similarity index 63%
rename from tools/ci-scripts/deb_package/Dockerfile_build_ubuntu-18.04
rename to tools/ci-scripts/deb_package/Dockerfile_build_ubuntu-24.04
index 0dc06e2e98..05274ae303 100644
--- a/tools/ci-scripts/deb_package/Dockerfile_build_ubuntu-18.04
+++ b/tools/ci-scripts/deb_package/Dockerfile_build_ubuntu-24.04
@@ -1,9 +1,9 @@
-# Copyright 2022-2023 Overte e.V.
+# Copyright 2022-2024 Overte e.V.
 # SPDX-License-Identifier: Apache-2.0
 
 # Docker file for building Overte Server
-# Example build: docker build -t overte/overte-server-build:0.1.3-ubuntu-18.04 -f Dockerfile_build_ubuntu-18.04 .
-FROM ubuntu:18.04
+# Example build: docker build -t overte/overte-server-build:0.1.3-ubuntu-24.04 -f Dockerfile_build_ubuntu-24.04 .
+FROM ubuntu:24.04
 LABEL maintainer="Julian Groß (julian.gro@overte.org)"
 LABEL description="Development image for Overte Domain server and assignment clients."
 
@@ -15,7 +15,9 @@ RUN echo UTC >/etc/timezone
 RUN apt-get update && apt-get -y install tzdata
 
 # Install Overte domain-server and assignment-client build dependencies
-RUN apt-get -y install curl ninja-build git g++ libssl-dev python3-distutils python3-distro mesa-common-dev libgl1-mesa-dev libharfbuzz-dev libdouble-conversion1 libsystemd-dev
+RUN apt-get -y install curl ninja-build git cmake g++ libssl-dev libqt5websockets5-dev qtdeclarative5-dev qtmultimedia5-dev python3-setuptools python3-distro mesa-common-dev libgl1-mesa-dev libsystemd-dev
+# Install Overte tools build dependencies
+RUN apt-get -y install libqt5webchannel5-dev qtwebengine5-dev libqt5xmlpatterns5-dev
 
 # Install tools for package creation
 RUN apt-get -y install sudo chrpath binutils dh-make
@@ -32,13 +34,5 @@ RUN echo "export LC_ALL=en_US.UTF-8" >> ~/.bashrc
 RUN echo "export LANG=en_US.UTF-8" >> ~/.bashrc
 RUN echo "export LANGUAGE=en_US.UTF-8" >> ~/.bashrc
 
-# Install tools for creating the server image
-RUN apt-get -y install docker.io xz-utils
-
 # Install tools needed for our Github Actions Workflow
-RUN apt-get -y install python3-boto3 python3-github zip
-
-# Install newer CMake
-RUN curl -O -L https://github.com/Kitware/CMake/releases/download/v3.24.0/cmake-3.24.0-linux-x86_64.tar.gz
-RUN tar -xf cmake-3.24.0-linux-x86_64.tar.gz
-ENV PATH="/cmake-3.24.0-linux-x86_64/bin:$PATH"
+Run apt-get -y install python3-boto3 python3-github zip
diff --git a/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-38 b/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-39
similarity index 65%
rename from tools/ci-scripts/rpm_package/Dockerfile_build_fedora-38
rename to tools/ci-scripts/rpm_package/Dockerfile_build_fedora-39
index c582ea6164..ef5ef24819 100644
--- a/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-38
+++ b/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-39
@@ -1,14 +1,14 @@
-# Copyright 2022-2023 Overte e.V.
+# Copyright 2022-2024 Overte e.V.
 # SPDX-License-Identifier: Apache-2.0
 
 # Docker file for building Overte Server
-# Example build: docker build -t overte/overte-server-build:0.1.3-fedora-38 -f Dockerfile_build_fedora-38 .
-FROM fedora:38
+# Example build: docker build -t overte/overte-server-build:0.1.4-fedora-39 -f Dockerfile_build_fedora-39 .
+FROM fedora:39
 LABEL maintainer="Julian Groß (julian.gro@overte.org)"
 LABEL description="Development image for Overte Domain server and assignment clients."
 
 # Install Overte domain-server and assignment-client build dependencies
-RUN dnf -y install curl ninja-build git cmake gcc-c++ openssl-devel qt5-qtwebsockets-devel qt5-qtmultimedia-devel unzip libXext-devel qt5-qtwebchannel-devel qt5-qtwebengine-devel qt5-qtxmlpatterns-devel systemd-devel
+RUN dnf -y install curl ninja-build git cmake gcc gcc-c++ openssl-devel qt5-qtwebsockets-devel qt5-qtmultimedia-devel unzip libXext-devel qt5-qtwebchannel-devel qt5-qtwebengine-devel qt5-qtxmlpatterns-devel systemd-devel python3.11
 
 # Install additional build tools
 RUN dnf -y install zip unzip
diff --git a/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-37 b/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-40
similarity index 65%
rename from tools/ci-scripts/rpm_package/Dockerfile_build_fedora-37
rename to tools/ci-scripts/rpm_package/Dockerfile_build_fedora-40
index b4131893c4..d9a9d994c3 100644
--- a/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-37
+++ b/tools/ci-scripts/rpm_package/Dockerfile_build_fedora-40
@@ -1,14 +1,14 @@
-# Copyright 2022-2023 Overte e.V.
+# Copyright 2022-2024 Overte e.V.
 # SPDX-License-Identifier: Apache-2.0
 
 # Docker file for building Overte Server
-# Example build: docker build -t overte/overte-server-build:0.1.3-fedora-37 -f Dockerfile_build_fedora-37 .
-FROM fedora:37
+# Example build: docker build -t overte/overte-server-build:0.1.4-fedora-40 -f Dockerfile_build_fedora-40 .
+FROM fedora:40
 LABEL maintainer="Julian Groß (julian.gro@overte.org)"
 LABEL description="Development image for Overte Domain server and assignment clients."
 
 # Install Overte domain-server and assignment-client build dependencies
-RUN dnf -y install curl ninja-build git cmake gcc-c++ openssl-devel qt5-qtwebsockets-devel qt5-qtmultimedia-devel unzip libXext-devel qt5-qtwebchannel-devel qt5-qtwebengine-devel qt5-qtxmlpatterns-devel systemd-devel
+RUN dnf -y install curl ninja-build git cmake gcc gcc-c++ openssl-devel qt5-qtwebsockets-devel qt5-qtmultimedia-devel unzip libXext-devel qt5-qtwebchannel-devel qt5-qtwebengine-devel qt5-qtxmlpatterns-devel systemd-devel python3
 
 # Install additional build tools
 RUN dnf -y install zip unzip
diff --git a/tools/jsdoc/plugins/hifi.js b/tools/jsdoc/plugins/hifi.js
index 463b049384..7a789e39ee 100644
--- a/tools/jsdoc/plugins/hifi.js
+++ b/tools/jsdoc/plugins/hifi.js
@@ -58,6 +58,7 @@ exports.handlers = {
             '../../libraries/plugins/src/plugins',
             '../../libraries/procedural/src/procedural',
             '../../libraries/pointers/src',
+            '../../libraries/recording/src/recording',
             '../../libraries/render-utils/src',
             '../../libraries/script-engine/src',
             '../../libraries/shared/src',
diff --git a/tools/nitpick/AppDataHighFidelity/Interface.json b/tools/nitpick/AppDataHighFidelity/Interface.json
index a9d27d8309..1c8ccad6e7 100644
--- a/tools/nitpick/AppDataHighFidelity/Interface.json
+++ b/tools/nitpick/AppDataHighFidelity/Interface.json
@@ -38,12 +38,12 @@
     "Avatar/animGraphURL": "",
     "Avatar/attachmentData/size": 0,
     "Avatar/avatarEntityData/size": 0,
-    "Avatar/collisionSoundURL": "https://cdn-1.vircadia.com/eu-c-1/vircadia-public/sounds/Collisions-otherorganic/Body_Hits_Impact.wav",
+    "Avatar/collisionSoundURL": "",
     "Avatar/displayName": "",
     "Avatar/dominantHand": "right",
     "Avatar/flyingHMD": false,
     "Avatar/fullAvatarModelName": "Default",
-    "Avatar/fullAvatarURL": "",
+    "fullPrivate/Avatar/fullAvatarURL": "",
     "Avatar/headPitch": 0,
     "Avatar/pitchSpeed": 75,
     "Avatar/scale": 1,
diff --git a/tools/nitpick/src/TestRunnerDesktop.cpp b/tools/nitpick/src/TestRunnerDesktop.cpp
index a7f0e6689d..ca288cae56 100644
--- a/tools/nitpick/src/TestRunnerDesktop.cpp
+++ b/tools/nitpick/src/TestRunnerDesktop.cpp
@@ -4,6 +4,7 @@
 //  Created by Nissim Hadar on 1 Sept 2018.
 //  Copyright 2013 High Fidelity, Inc.
 //  Copyright 2020 Vircadia contributors.
+//  Copyright 2024 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
@@ -177,7 +178,7 @@ void TestRunnerDesktop::run() {
     _user = nitpick->getSelectedUser();
 
     // This will be restored at the end of the tests
-    saveExistingHighFidelityAppDataFolder();
+    saveExistingAppDataFolder();
 
     if (_usePreviousInstallationCheckBox->isChecked()) {
         installationComplete();
@@ -266,8 +267,8 @@ void TestRunnerDesktop::runInstaller() {
         folderName += QString(" - ") + getPRNumberFromURL(_url->text());
     }
 
-    script.write((QString("cp -Rf \"$VOLUME/") + folderName + "/interface.app\" \"" + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str());
-    script.write((QString("cp -Rf \"$VOLUME/") + folderName + "/Sandbox.app\" \""   + _workingFolder + "/High_Fidelity/\"\n").toStdString().c_str());
+    script.write((QString("cp -Rf \"$VOLUME/") + folderName + "/interface.app\" \"" + _workingFolder + "/Overte/\"\n").toStdString().c_str());
+    script.write((QString("cp -Rf \"$VOLUME/") + folderName + "/Sandbox.app\" \""   + _workingFolder + "/Overte/\"\n").toStdString().c_str());
 
     script.write("hdiutil detach \"$VOLUME\"\n");
     script.write("killall yes\n");
@@ -313,7 +314,7 @@ void TestRunnerDesktop::verifyInstallationSucceeded() {
 #endif
 }
 
-void TestRunnerDesktop::saveExistingHighFidelityAppDataFolder() {
+void TestRunnerDesktop::saveExistingAppDataFolder() {
     QString dataDirectory{ "NOT FOUND" };
 #ifdef Q_OS_WIN
     dataDirectory = qgetenv("USERPROFILE") + "\\AppData\\Roaming";
@@ -339,9 +340,9 @@ void TestRunnerDesktop::saveExistingHighFidelityAppDataFolder() {
     // Copy an "empty" AppData folder (i.e. no entities)
     QDir canonicalAppDataFolder;
 #ifdef Q_OS_WIN
-    canonicalAppDataFolder = QDir::currentPath() + "/AppDataHighFidelity";
+    canonicalAppDataFolder.setPath(QDir::currentPath() + "/AppDataOverte");
 #elif defined Q_OS_MAC
-    canonicalAppDataFolder = QCoreApplication::applicationDirPath() + "/AppDataHighFidelity";
+    canonicalAppDataFolder.setPath(QCoreApplication::applicationDirPath() + "/AppDataOverte");
 #endif
     if (canonicalAppDataFolder.exists()) {
         copyFolder(canonicalAppDataFolder.path(), _appDataFolder.path());
@@ -566,7 +567,7 @@ void TestRunnerDesktop::evaluateResults() {
 
 void TestRunnerDesktop::automaticTestRunEvaluationComplete(const QString& zippedFolder, int numberOfFailures) {
     addBuildNumberToResults(zippedFolder);
-    restoreHighFidelityAppDataFolder();
+    restoreAppDataFolder();
 
     _statusLabel->setText("Testing complete");
 
@@ -604,7 +605,7 @@ void TestRunnerDesktop::addBuildNumberToResults(const QString& zippedFolderName)
     }
 }
 
-void TestRunnerDesktop::restoreHighFidelityAppDataFolder() {
+void TestRunnerDesktop::restoreAppDataFolder() {
     _appDataFolder.removeRecursively();
 
     if (_savedAppDataFolder != QDir()) {
diff --git a/tools/nitpick/src/TestRunnerDesktop.h b/tools/nitpick/src/TestRunnerDesktop.h
index dce2dce2ba..ba9c66ac74 100644
--- a/tools/nitpick/src/TestRunnerDesktop.h
+++ b/tools/nitpick/src/TestRunnerDesktop.h
@@ -3,6 +3,7 @@
 //
 //  Created by Nissim Hadar on 1 Sept 2018.
 //  Copyright 2013 High Fidelity, Inc.
+//  Copyright 2024 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
@@ -52,8 +53,8 @@ public:
     void runInstaller();
     void verifyInstallationSucceeded();
 
-    void saveExistingHighFidelityAppDataFolder();
-    void restoreHighFidelityAppDataFolder();
+    void saveExistingAppDataFolder();
+    void restoreAppDataFolder();
 
     void createSnapshotFolder();
     
diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs
index ba8eaea8b5..7410bbdff0 100644
--- a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs
+++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/AvatarExporter.cs
@@ -15,12 +15,13 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Globalization;
-
+using System.Linq;
+using Overte;
 
 class AvatarExporter : MonoBehaviour
 {
     // update version number for every PR that changes this file, also set updated version in README file
-    static readonly string AVATAR_EXPORTER_VERSION = "0.6.0";
+    public static readonly string AVATAR_EXPORTER_VERSION = "2023.08";
 
     static readonly float HIPS_MIN_Y_PERCENT_OF_HEIGHT = 0.03f;
     static readonly float BELOW_GROUND_THRESHOLD_PERCENT_OF_HEIGHT = -0.15f;
@@ -186,9 +187,11 @@ class AvatarExporter : MonoBehaviour
     };
 
     static readonly string STANDARD_SHADER = "Standard";
+    static readonly string STANDARD_ROUGHNESS_SHADER = "Autodesk Interactive"; // "Standard (Roughness setup)" Has been renamed in unity 2018.03
     static readonly string STANDARD_SPECULAR_SHADER = "Standard (Specular setup)";
     static readonly string[] SUPPORTED_SHADERS = new string[] {
         STANDARD_SHADER,
+        STANDARD_ROUGHNESS_SHADER,
         STANDARD_SPECULAR_SHADER,
     };
 
@@ -223,106 +226,13 @@ class AvatarExporter : MonoBehaviour
         AvatarRule.HeadMapped,
     };
 
-    class UserBoneInformation
-    {
-        public string humanName; // bone name in Humanoid if it is mapped, otherwise ""
-        public string parentName; // parent user bone name
-        public BoneTreeNode boneTreeNode; // node within the user bone tree
-        public int mappingCount; // number of times this bone is mapped in Humanoid
-        public Vector3 position; // absolute position
-        public Quaternion rotation; // absolute rotation
-
-        public UserBoneInformation()
-        {
-            humanName = "";
-            parentName = "";
-            boneTreeNode = new BoneTreeNode();
-            mappingCount = 0;
-            position = new Vector3();
-            rotation = new Quaternion();
-        }
-        public UserBoneInformation(string parent, BoneTreeNode treeNode, Vector3 pos)
-        {
-            humanName = "";
-            parentName = parent;
-            boneTreeNode = treeNode;
-            mappingCount = 0;
-            position = pos;
-            rotation = new Quaternion();
-        }
-
-        public bool HasHumanMapping() { return !string.IsNullOrEmpty(humanName); }
-    }
-
-    class BoneTreeNode
-    {
-        public string boneName;
-        public string parentName;
-        public List<BoneTreeNode> children = new List<BoneTreeNode>();
-
-        public BoneTreeNode() { }
-        public BoneTreeNode(string name, string parent)
-        {
-            boneName = name;
-            parentName = parent;
-        }
-    }
-
-    class MaterialData
-    {
-        public Color albedo;
-        public string albedoMap;
-        public double metallic;
-        public string metallicMap;
-        public double roughness;
-        public string roughnessMap;
-        public string normalMap;
-        public string occlusionMap;
-        public Color emissive;
-        public string emissiveMap;
-
-        public string getJSON()
-        {
-            string json = "{ \"materialVersion\": 1, \"materials\": { ";
-
-            //Albedo
-            json += $"\"albedo\": [{albedo.r.F()}, {albedo.g.F()}, {albedo.b.F()}], ";
-            if (!string.IsNullOrEmpty(albedoMap))
-                json += $"\"albedoMap\": \"{albedoMap}\", ";
-
-            //Metallic
-            json += $"\"metallic\": {metallic.F()}, ";
-            if (!string.IsNullOrEmpty(metallicMap))
-                json += $"\"metallicMap\": \"{metallicMap}\", ";
-
-            //Roughness
-            json += $"\"roughness\": {roughness.F()}, ";
-            if (!string.IsNullOrEmpty(roughnessMap))
-                json += $"\"roughnessMap\": \"{roughnessMap}\", ";
-
-            //Normal
-            if (!string.IsNullOrEmpty(normalMap))
-                json += $"\"normalMap\": \"{normalMap}\", ";
-
-            //Occlusion
-            if (!string.IsNullOrEmpty(occlusionMap))
-                json += $"\"occlusionMap\": \"{occlusionMap}\", ";
-
-            //Emissive
-            json += $"\"emissive\": [{emissive.r.F()}, {emissive.g.F()}, {emissive.b.F()}]";
-            if (!string.IsNullOrEmpty(emissiveMap))
-                json += $", \"emissiveMap\": \"{emissiveMap}\"";
-
-            json += " } }";
-            return json;
-        }
-    }
-
     static string assetPath = "";
     static string assetName = "";
     static ModelImporter modelImporter;
     static HumanDescription humanDescription;
 
+    static FST currentFst;
+
     static Dictionary<string, UserBoneInformation> userBoneInfos = new Dictionary<string, UserBoneInformation>();
     static Dictionary<string, string> humanoidToUserBoneMappings = new Dictionary<string, string>();
     static BoneTreeNode userBoneTree = new BoneTreeNode();
@@ -359,7 +269,8 @@ class AvatarExporter : MonoBehaviour
     [MenuItem("Overte/About")]
     static void About()
     {
-        EditorUtility.DisplayDialog("About", "Avatar Exporter\nVersion " + AVATAR_EXPORTER_VERSION + "\nCopyright 2022 to 2023 Overte e.V.\nCopyright 2018 High Fidelity, Inc.", "Ok");
+        EditorUtility.DisplayDialog("About",
+            $"Avatar Exporter\nVersion {AVATAR_EXPORTER_VERSION}\nCopyright 2022 to 2023 Overte e.V.\nCopyright 2018 High Fidelity, Inc.", "Ok");
     }
 
     static void ExportSelectedAvatar(bool updateExistingAvatar)
@@ -427,14 +338,10 @@ class AvatarExporter : MonoBehaviour
         warnings = "";
         foreach (var failedAvatarRule in failedAvatarRules)
         {
-            if (Array.IndexOf(EXPORT_BLOCKING_AVATAR_RULES, failedAvatarRule.Key) >= 0)
-            {
+            if (EXPORT_BLOCKING_AVATAR_RULES.Contains(failedAvatarRule.Key))
                 boneErrors += failedAvatarRule.Value + "\n\n";
-            }
             else
-            {
                 warnings += failedAvatarRule.Value + "\n\n";
-            }
         }
 
         // add material and texture warnings after bone-related warnings
@@ -443,9 +350,7 @@ class AvatarExporter : MonoBehaviour
 
         // remove trailing newlines at the end of the warnings
         if (!string.IsNullOrEmpty(warnings))
-        {
-            warnings = warnings.Substring(0, warnings.LastIndexOf("\n\n"));
-        }
+            warnings = warnings.Trim();
 
         if (!string.IsNullOrEmpty(boneErrors))
         {
@@ -456,7 +361,7 @@ class AvatarExporter : MonoBehaviour
                 boneErrors += "Warnings:\n\n" + warnings;
             }
             // remove ending newlines from the last rule failure string that was added above
-            boneErrors = boneErrors.Substring(0, boneErrors.LastIndexOf("\n\n"));
+            boneErrors = boneErrors.Trim();
             EditorUtility.DisplayDialog("Error", boneErrors, "Ok");
             return;
         }
@@ -502,33 +407,25 @@ class AvatarExporter : MonoBehaviour
     {
         bool copyModelToExport = false;
 
-        // lookup the project name field from the fst file to update
-        projectName = "";
         try
         {
-            string[] lines = File.ReadAllLines(exportFstPath);
-            foreach (string line in lines)
+            currentFst = new FST();
+            // load the old file first
+            if(!currentFst.LoadFile(exportFstPath))
             {
-                int separatorIndex = line.IndexOf("=");
-                if (separatorIndex >= 0)
-                {
-                    string key = line.Substring(0, separatorIndex).Trim();
-                    if (key == "name")
-                    {
-                        projectName = line.Substring(separatorIndex + 1).Trim();
-                        break;
-                    }
-                }
+                EditorUtility.DisplayDialog("Error",
+                    $"Failed to read from existing file {exportFstPath}. Please check the file and try again.", "Ok");
+                return;
             }
         }
         catch
         {
-            EditorUtility.DisplayDialog("Error", "Failed to read from existing file " + exportFstPath +
-                                        ". Please check the file and try again.", "Ok");
+            EditorUtility.DisplayDialog("Error",
+                $"Failed to read from existing file {exportFstPath}. Please check the file and try again.", "Ok");
             return;
         }
 
-        string exportModelPath = Path.GetDirectoryName(exportFstPath) + "/" + assetName + ".fbx";
+        string exportModelPath = $"{Path.GetDirectoryName(exportFstPath)}/{assetName}.fbx";
         if (File.Exists(exportModelPath))
         {
             // if the fbx in Unity Assets is newer than the fbx in the target export
@@ -613,21 +510,10 @@ class AvatarExporter : MonoBehaviour
             }
         }
 
-        // delete existing fst file since we will write a new file
-        // TODO: updating fst should only rewrite joint mappings and joint rotation offsets to existing file
-        try
-        {
-            File.Delete(exportFstPath);
-        }
-        catch
-        {
-            EditorUtility.DisplayDialog("Error", "Failed to overwrite existing file " + exportFstPath +
-                                        ". Please check the file and try again.", "Ok");
-            return;
-        }
+        currentFst.scale = scale;
 
         // write out a new fst file in place of the old file
-        if (!WriteFST(exportFstPath, projectName, scale))
+        if (!WriteFST(exportFstPath))
         {
             return;
         }
@@ -662,7 +548,14 @@ class AvatarExporter : MonoBehaviour
 
         // write out the avatar.fst file to the project directory
         string exportFstPath = projectDirectory + "avatar.fst";
-        if (!WriteFST(exportFstPath, projectName, scale))
+
+        currentFst = new FST
+        {
+            name = projectName,
+            scale = scale,
+            filename = $"{assetName}.fbx"
+        };
+        if (!WriteFST(exportFstPath))
         {
             return;
         }
@@ -692,39 +585,24 @@ class AvatarExporter : MonoBehaviour
     }
 
     // The Overte FBX Serializer omits the colon based prefixes. This will make the jointnames compatible.
-    static string removeTypeFromJointname(string jointName)
-    {
-        return jointName.Substring(jointName.IndexOf(':') + 1);
-    }
+    static string removeTypeFromJointname(string jointName) => jointName.Substring(jointName.IndexOf(':') + 1);
 
-    static bool WriteFST(string exportFstPath, string projectName, float scale)
+    static bool WriteFST(string exportFstPath)
     {
-        // write out core fields to top of fst file
-        try
-        {
-            File.WriteAllText(exportFstPath,
-                $"exporterVersion = {AVATAR_EXPORTER_VERSION}\n" +
-                $"name     = {projectName}\n" +
-                "type     = body+head\n" +
-                $"scale    = {scale.F()}\n" +
-                $"filename = {assetName}.fbx\n" +
-                "texdir   = textures\n"
-            );
-        }
-        catch
-        {
-            EditorUtility.DisplayDialog("Error", "Failed to write file " + exportFstPath +
-                                        ". Please check the location and try again.", "Ok");
-            return false;
-        }
-
         // write out joint mappings to fst file
         foreach (var userBoneInfo in userBoneInfos)
         {
             if (userBoneInfo.Value.HasHumanMapping())
             {
-                string overteJointName = HUMANOID_TO_OVERTE_JOINT_NAME[userBoneInfo.Value.humanName];
-                File.AppendAllText(exportFstPath, $"jointMap = {overteJointName} = {removeTypeFromJointname(userBoneInfo.Key)}\n");
+                var jointName = HUMANOID_TO_OVERTE_JOINT_NAME[userBoneInfo.Value.humanName];
+                var userJointName = removeTypeFromJointname(userBoneInfo.Key);
+                // Skip joints with the same name
+                if(jointName == userJointName)
+                    continue;
+                if (!currentFst.jointMapList.Exists(x => x.From == jointName))
+                    currentFst.jointMapList.Add(new JointMap(jointName, userJointName));
+                else
+                    currentFst.jointMapList.Find(x => x.From == jointName).To = userJointName;
             }
         }
 
@@ -772,11 +650,15 @@ class AvatarExporter : MonoBehaviour
                 }
             }
 
-            // swap from left-handed (Unity) to right-handed (Overte) coordinates and write out joint rotation offset to fst
-            jointOffset = new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w);
-            File.AppendAllText(exportFstPath,
-                $"jointRotationOffset2 = {removeTypeFromJointname(userBoneName)} = ({jointOffset.x.F()}, {jointOffset.y.F()}, {jointOffset.z.F()}, {jointOffset.w.F()})\n"
-            );
+            var norBName = removeTypeFromJointname(userBoneName);
+            if (!currentFst.jointRotationList.Exists(x => x.BoneName == norBName))
+                // swap from left-handed (Unity) to right-handed (Overte) coordinates and write out joint rotation offset to fst
+                currentFst.jointRotationList.Add(
+                    new JointRotationOffset2(norBName, -jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w)
+                );
+            else
+                currentFst.jointRotationList.Find(x => x.BoneName == norBName).offset =
+                    new Quaternion(-jointOffset.x, jointOffset.y, jointOffset.z, -jointOffset.w);
         }
 
         // if there is any material data to save then write out all materials in JSON material format to the materialMap field
@@ -788,11 +670,13 @@ class AvatarExporter : MonoBehaviour
                 // if this is the only material in the mapping and it is mapped to default material name No Name,
                 // then the avatar has no embedded materials and this material should be applied to all meshes
                 string matName = (materialMappings.Count == 1 && materialData.Key == DEFAULT_MATERIAL_NAME) ? "all" : $"mat::{materialData.Key}";
-                matData.Add($"\"{matName}\": {materialData.Value.getJSON()}");
+                matData.Add($"\"{matName}\": {materialData.Value}");
             }
-            File.AppendAllText(exportFstPath, $"materialMap = {{{string.Join(",", matData)}}}");
+            currentFst.materialMap = $"{{{string.Join(",", matData)}}}";
         }
 
+        var res = currentFst.ExportFile(exportFstPath);
+
         EditorPrefs.SetString("OV_LAST_PROJECT_PATH", exportFstPath);
 
         /*if (SystemInfo.operatingSystemFamily == OperatingSystemFamily.Windows)
@@ -801,7 +685,7 @@ class AvatarExporter : MonoBehaviour
             System.Diagnostics.Process.Start("explorer.exe", "/select," + exportFstPath);
         }*/
 
-        return true;
+        return res;
     }
 
     static void SetBoneAndMaterialInformation()
@@ -831,11 +715,10 @@ class AvatarExporter : MonoBehaviour
         {
             string humanName = bone.humanName;
             string userBoneName = bone.boneName;
-            string overteJointName;
             if (userBoneInfos.ContainsKey(userBoneName))
             {
                 ++userBoneInfos[userBoneName].mappingCount;
-                if (HUMANOID_TO_OVERTE_JOINT_NAME.TryGetValue(humanName, out overteJointName))
+                if (HUMANOID_TO_OVERTE_JOINT_NAME.ContainsKey(humanName))
                 {
                     userBoneInfos[userBoneName].humanName = humanName;
                     humanoidToUserBoneMappings.Add(humanName, userBoneName);
@@ -1350,8 +1233,9 @@ class AvatarExporter : MonoBehaviour
             }
 
             // don't store any material data for unsupported shader types
-            if (Array.IndexOf(SUPPORTED_SHADERS, shaderName) == -1)
+            if (!SUPPORTED_SHADERS.Contains(shaderName))
             {
+                Debug.LogWarning($"Unsuported shader {shaderName} in mat {materialName}");
                 if (!unsupportedShaderMaterials.Contains(materialName))
                 {
                     unsupportedShaderMaterials.Add(materialName);
@@ -1444,46 +1328,22 @@ class AvatarExporter : MonoBehaviour
 
     static void AddMaterialWarnings()
     {
-        string alternateStandardShaders = "";
-        string unsupportedShaders = "";
-        // combine all material names for each material warning into a comma-separated string
-        foreach (string materialName in alternateStandardShaderMaterials)
+        if (alternateStandardShaderMaterials.Count != 0)
         {
-            if (!string.IsNullOrEmpty(alternateStandardShaders))
-            {
-                alternateStandardShaders += ", ";
-            }
-            alternateStandardShaders += materialName;
+            string alternateStandardShaders = string.Join(", ", alternateStandardShaderMaterials);
+            warnings += alternateStandardShaderMaterials.Count == 1
+                ? $"The material {alternateStandardShaders} is not using the recommended variation of the Standard shader."
+                : $"The materials {alternateStandardShaders} are not using the recommended variation of the Standard shader."
+                  + " We recommend you change them to \"Autodesk Interactive\" shader for improved performance.\n\n";
         }
-        foreach (string materialName in unsupportedShaderMaterials)
+
+        if (unsupportedShaderMaterials.Count != 0)
         {
-            if (!string.IsNullOrEmpty(unsupportedShaders))
-            {
-                unsupportedShaders += ", ";
-            }
-            unsupportedShaders += materialName;
-        }
-        if (alternateStandardShaderMaterials.Count > 1)
-        {
-            warnings += "The materials " + alternateStandardShaders + " are not using the " +
-                        "recommended variation of the Standard shader. We recommend you change " +
-                        "them to Standard (Roughness setup) shader for improved performance.\n\n";
-        }
-        else if (alternateStandardShaderMaterials.Count == 1)
-        {
-            warnings += "The material " + alternateStandardShaders + " is not using the " +
-                        "recommended variation of the Standard shader. We recommend you change " +
-                        "it to Standard (Roughness setup) shader for improved performance.\n\n";
-        }
-        if (unsupportedShaderMaterials.Count > 1)
-        {
-            warnings += "The materials " + unsupportedShaders + " are using an unsupported shader. " +
-                        "We recommend you change them to a Standard shader type.\n\n";
-        }
-        else if (unsupportedShaderMaterials.Count == 1)
-        {
-            warnings += "The material " + unsupportedShaders + " is using an unsupported shader. " +
-                        "We recommend you change it to a Standard shader type.\n\n";
+            string unsupportedShaders = string.Join(", ", unsupportedShaderMaterials);
+            warnings += unsupportedShaderMaterials.Count == 1
+                ? $"The material {unsupportedShaders} is using an unsupported shader."
+                : $"The materials {unsupportedShaders} are using an unsupported shader."
+                  + " We recommend you change it to the \"Autodesk Interactive\" shader\n\n";
         }
     }
 
@@ -1917,11 +1777,6 @@ class AvatarUtilities
     }
 }
 
-public static class ConverterExtensions
-{
-    //Helper function to convert floats to string without commas
-    public static string F(this float x) => x.ToString("G", CultureInfo.InvariantCulture);
-    public static string F(this double x) => x.ToString("G", CultureInfo.InvariantCulture);
-}
+
 
 #endif
diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/FST.cs b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/FST.cs
new file mode 100644
index 0000000000..2574e6064d
--- /dev/null
+++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/FST.cs
@@ -0,0 +1,402 @@
+//  FST.cs
+//
+//  Created by Edgar on 24-8-2023
+//  Copyright 2023 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
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using UnityEditor;
+using UnityEngine;
+using System.Globalization;
+
+namespace Overte
+{
+    class JointMap
+    {
+        public string From;
+        public string To;
+
+        private Regex parseRx = new Regex(@"^(?<From>[\w]*)\s*=\s*(?<To>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+        public JointMap(string RawInput)
+        {
+            var parsed = parseRx.Matches(RawInput)[0];
+            From = parsed.Groups["From"].Value.Trim();
+            To = parsed.Groups["To"].Value.Trim();
+        }
+
+        public JointMap(string f, string t)
+        {
+            From = f; To = t;
+        }
+
+        public override string ToString() => $"jointMap = {From} = {To}";
+    }
+    
+    class Joint
+    {
+        public string From;
+        public string To;
+
+        private Regex parseRx = new Regex(@"^(?<From>[\w]*)\s*=\s*(?<To>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+        public Joint(string RawInput)
+        {
+            var parsed = parseRx.Matches(RawInput)[0];
+            From = parsed.Groups["From"].Value.Trim();
+            To = parsed.Groups["To"].Value.Trim();
+        }
+
+        public Joint(string f, string t)
+        {
+            From = f; To = t;
+        }
+
+        public override string ToString() => $"joint = {From} = {To}";
+    }
+
+    class JointRotationOffset2
+    {
+        public string BoneName;
+        public Quaternion offset;
+
+        private Regex parseRx = new Regex(@"(?<BoneName>.*)\s*=\s*\(\s*(?<X>.*)\s*,\s*(?<Y>.*)\s*,\s*(?<Z>.*)\s*,\s*(?<W>.*)\s*\)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+        public JointRotationOffset2(string value)
+        {
+            var parsed = parseRx.Matches(value)[0];
+            BoneName = parsed.Groups["BoneName"].Value.Trim();
+            offset = new Quaternion
+            {
+                x = float.Parse(parsed.Groups["X"].Value, CultureInfo.InvariantCulture),
+                y = float.Parse(parsed.Groups["Y"].Value, CultureInfo.InvariantCulture),
+                z = float.Parse(parsed.Groups["Z"].Value, CultureInfo.InvariantCulture),
+                w = float.Parse(parsed.Groups["W"].Value, CultureInfo.InvariantCulture)
+            };
+        }
+
+        public JointRotationOffset2(string boneName, float x, float y, float z, float w)
+        {
+            BoneName = boneName;
+            offset = new Quaternion(x, y, z, w);
+        }
+
+        public override string ToString() => $"jointRotationOffset2 = {BoneName} = ({offset.x.F()}, {offset.y.F()}, {offset.z.F()}, {offset.w.F()})";
+    }
+
+    class RemapBlendShape
+    {
+        public string From;
+        public string To;
+        public float Multiplier;
+
+        private Regex parseRx = new Regex(@"(?<From>.*)\s*=\s*(?<To>.*)\s*=\s*(?<Multiplier>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+        public RemapBlendShape(string rawData)
+        {
+            var parsed = parseRx.Matches(rawData)[0];
+            From = parsed.Groups["From"].Value.Trim();
+            To = parsed.Groups["To"].Value.Trim();
+            Multiplier = float.Parse(parsed.Groups["Multiplier"].Value, CultureInfo.InvariantCulture);
+        }
+
+        public override string ToString() => $"bs = {From} = {To} = {Multiplier.F()}";
+    }
+
+    class JointIndex
+    {
+        public string BoneName;
+        public int Index;
+
+        private Regex parseRx = new Regex(@"^(?<BoneName>.*)\s*=\s*(?<Index>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+        public JointIndex(string rawData)
+        {
+            var parsed = parseRx.Matches(rawData)[0];
+            BoneName = parsed.Groups["BoneName"].Value.Trim();
+            Index = int.Parse(parsed.Groups["Index"].Value);
+        }
+        
+        public override string ToString() => $"jointIndex = {BoneName} = {Index}";
+    }
+
+    class FST
+    {
+        public readonly string exporterVersion = AvatarExporter.AVATAR_EXPORTER_VERSION;
+        public string name;
+        public string type = "body+head";
+        public float scale = 1.0f;
+        public string filename;
+        public string texdir = "textures";
+        public string materialMap;
+        public string script;
+
+        public List<RemapBlendShape> remapBlendShapeList = new List<RemapBlendShape>();
+
+        public List<Joint> jointList = new List<Joint>();
+        public List<JointMap> jointMapList = new List<JointMap>();
+        public List<JointRotationOffset2> jointRotationList = new List<JointRotationOffset2>();
+        public List<JointIndex> jointIndexList = new List<JointIndex>();
+        public List<string> freeJointList = new List<string>();
+
+        private Regex parseRx = new Regex(@"^(?<Key>[\w]*)\s*=\s*(?<Value>.*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+        public string flowPhysicsData;
+        public string flowCollisionsData;
+
+        public string lod;
+        public string joint;
+
+        List<string> fstContent = new List<string>();
+
+        public bool ExportFile(string fstPath)
+        {
+            fstContent = new List<string>
+            {
+                $"exporterVersion = {exporterVersion}",
+                $"name     = {name}",
+                $"type     = {type}",
+                $"scale    = {scale.F()}",
+                $"filename = {filename}",
+                $"texdir   = {texdir}"
+            };
+            AddIfNotNull(remapBlendShapeList);
+            AddIfNotNull(jointMapList);
+            AddIfNotNull(jointIndexList);
+            AddIfNotNull(jointRotationList);
+            AddIfNotNull("freeJoint", freeJointList);
+
+            AddIfNotNull(nameof(materialMap), materialMap);
+            AddIfNotNull(nameof(flowPhysicsData), flowPhysicsData);
+            AddIfNotNull(nameof(flowCollisionsData), flowCollisionsData);
+            AddIfNotNull(nameof(lod), lod);
+            AddIfNotNull(nameof(joint), joint);
+            AddIfNotNull(nameof(script), script);
+
+            try
+            {
+                System.IO.File.WriteAllLines(fstPath, fstContent);
+                return true;
+            }
+            catch (Exception e)
+            {
+                EditorUtility.DisplayDialog("Error", "Failed to write file " + fstPath +
+                                            ". Please check the location and try again.", "Ok");
+                Debug.LogException(e);
+                return false;
+            }
+        }
+
+        private void AddIfNotNull<T>(string keyname, List<T> list)
+        {
+            if (list.Count != 0)
+                list.ForEach(x => fstContent.Add($"{keyname} = {x}"));
+        }
+
+        private void AddIfNotNull<T>(List<T> list)
+        {
+            if (list.Count != 0)
+                fstContent.Add(string.Join("\n", list));
+        }
+
+        private void AddIfNotNull(string keyname, string valname)
+        {
+            if (!string.IsNullOrEmpty(valname))
+                fstContent.Add($"{keyname} = {valname}");
+        }
+
+
+        public bool LoadFile(string fstPath)
+        {
+            try
+            {
+                var rawFst = System.IO.File.ReadAllLines(fstPath);
+
+                foreach (var l in rawFst)
+                {
+                    if (!parseRx.IsMatch(l)) continue;
+                    var match = parseRx.Matches(l)[0];
+                    ParseLine(match.Groups["Key"].Value.Trim(), match.Groups["Value"].Value.Trim());
+                }
+
+                return true;
+            }
+            catch (Exception e)
+            {
+                EditorUtility.DisplayDialog("Error", "Failed to read file " + fstPath +
+                                            ". Please check the location and try again.", "Ok");
+                Debug.LogException(e);
+                return false;
+            }
+        }
+
+        private void ParseLine(string key, string value)
+        {
+            switch (key)
+            {
+                case "exporterVersion":
+                    //Just ingnore the old exporterVersion
+                    break;
+                case "name":
+                    name = value;
+                    break;
+                case "type":
+                    type = value;
+                    break;
+                case "scale":
+                    scale = float.Parse(value, CultureInfo.InvariantCulture);
+                    break;
+                case "filename":
+                    filename = value;
+                    break;
+                case "texdir":
+                    texdir = value;
+                    break;
+                case "materialMap":
+                    // The materialMap will be generated by unity, no need to parse it
+                    // TODO:Parse it when changed to importing instead of updating
+                    break;
+
+                case "joint":
+                    jointList.Add(new Joint(value));
+                    break;
+                case "jointMap":
+                    jointMapList.Add(new JointMap(value));
+                    break;
+                case "jointRotationOffset":
+                    // Old version, does not seem to be used
+                    break;
+                case "jointRotationOffset2":
+                    jointRotationList.Add(new JointRotationOffset2(value));
+                    break;
+                case "jointIndex":
+                    jointIndexList.Add(new JointIndex(value));
+                    break;
+                case "freeJoint":
+                    freeJointList.Add(value);
+                    break;
+
+                case "bs":
+                    remapBlendShapeList.Add(new RemapBlendShape(value));
+                    break;
+
+                default:
+                    Debug.LogError($"Unknown key \"{key}\"\nPlease report this issue on the issue tracker");
+                    break;
+            }
+        }
+
+        private KeyValuePair<string, string> ParseKVPair(Regex rx, string sinput)
+        {
+            var match = rx.Matches(sinput)[0];
+            return new KeyValuePair<string, string>(match.Groups["Key"].Value.Trim(), match.Groups["Value"].Value.Trim());
+        }
+    }
+
+    class UserBoneInformation
+    {
+        public string humanName; // bone name in Humanoid if it is mapped, otherwise ""
+        public string parentName; // parent user bone name
+        public BoneTreeNode boneTreeNode; // node within the user bone tree
+        public int mappingCount; // number of times this bone is mapped in Humanoid
+        public Vector3 position; // absolute position
+        public Quaternion rotation; // absolute rotation
+
+        public UserBoneInformation()
+        {
+            humanName = "";
+            parentName = "";
+            boneTreeNode = new BoneTreeNode();
+            mappingCount = 0;
+            position = new Vector3();
+            rotation = new Quaternion();
+        }
+        public UserBoneInformation(string parent, BoneTreeNode treeNode, Vector3 pos)
+        {
+            humanName = "";
+            parentName = parent;
+            boneTreeNode = treeNode;
+            mappingCount = 0;
+            position = pos;
+            rotation = new Quaternion();
+        }
+
+        public bool HasHumanMapping() { return !string.IsNullOrEmpty(humanName); }
+    }
+
+    class BoneTreeNode
+    {
+        public string boneName;
+        public string parentName;
+        public List<BoneTreeNode> children = new List<BoneTreeNode>();
+
+        public BoneTreeNode() { }
+        public BoneTreeNode(string name, string parent)
+        {
+            boneName = name;
+            parentName = parent;
+        }
+    }
+
+    class MaterialData
+    {
+        public Color albedo;
+        public string albedoMap;
+        public double metallic;
+        public string metallicMap;
+        public double roughness;
+        public string roughnessMap;
+        public string normalMap;
+        public string occlusionMap;
+        public Color emissive;
+        public string emissiveMap;
+
+        public override string ToString()
+        {
+            string json = "{ \"materialVersion\": 1, \"materials\": { ";
+
+            //Albedo
+            json += $"\"albedo\": [{albedo.r.F()}, {albedo.g.F()}, {albedo.b.F()}], ";
+            if (!string.IsNullOrEmpty(albedoMap))
+                json += $"\"albedoMap\": \"{albedoMap}\", ";
+
+            //Metallic
+            json += $"\"metallic\": {metallic.F()}, ";
+            if (!string.IsNullOrEmpty(metallicMap))
+                json += $"\"metallicMap\": \"{metallicMap}\", ";
+
+            //Roughness
+            json += $"\"roughness\": {roughness.F()}, ";
+            if (!string.IsNullOrEmpty(roughnessMap))
+                json += $"\"roughnessMap\": \"{roughnessMap}\", ";
+
+            //Normal
+            if (!string.IsNullOrEmpty(normalMap))
+                json += $"\"normalMap\": \"{normalMap}\", ";
+
+            //Occlusion
+            if (!string.IsNullOrEmpty(occlusionMap))
+                json += $"\"occlusionMap\": \"{occlusionMap}\", ";
+
+            //Emissive
+            json += $"\"emissive\": [{emissive.r.F()}, {emissive.g.F()}, {emissive.b.F()}]";
+            if (!string.IsNullOrEmpty(emissiveMap))
+                json += $", \"emissiveMap\": \"{emissiveMap}\"";
+
+            json += " } }";
+            return json;
+        }
+    }
+
+    public static class ConverterExtensions
+    {
+        //Helper function to convert floats to string without commas
+        public static string F(this float x) => x.ToString("G", CultureInfo.InvariantCulture);
+        public static string F(this double x) => x.ToString("G", CultureInfo.InvariantCulture);
+    }
+}
\ No newline at end of file
diff --git a/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/FST.cs.meta b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/FST.cs.meta
new file mode 100644
index 0000000000..8af30ca3ca
--- /dev/null
+++ b/tools/unity-avatar-exporter/Assets/Editor/AvatarExporter/FST.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 2472151ab437fca478d4c48d8d010c49
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/tools/unity-avatar-exporter/Assets/README.txt b/tools/unity-avatar-exporter/Assets/README.txt
index a1ac7bedd3..6d10f07a24 100644
--- a/tools/unity-avatar-exporter/Assets/README.txt
+++ b/tools/unity-avatar-exporter/Assets/README.txt
@@ -1,5 +1,5 @@
 Avatar Exporter
-Version 0.6.0
+Version 2023.08
 Copyright 2018 High Fidelity, Inc.
 Copyright 2022 Overte e.V.
 
diff --git a/tools/unity-avatar-exporter/avatarExporter.unitypackage b/tools/unity-avatar-exporter/avatarExporter.unitypackage
index 450c44b4f8..81cbac50f2 100644
Binary files a/tools/unity-avatar-exporter/avatarExporter.unitypackage and b/tools/unity-avatar-exporter/avatarExporter.unitypackage differ
diff --git a/unpublishedScripts/marketplace/gameTable/assets/cards/Leather0100-bump.jpg b/unpublishedScripts/marketplace/gameTable/assets/cards/Leather0100-bump.jpg
deleted file mode 100644
index de112a6d51..0000000000
Binary files a/unpublishedScripts/marketplace/gameTable/assets/cards/Leather0100-bump.jpg and /dev/null differ
diff --git a/unpublishedScripts/marketplace/gameTable/assets/cards/Leather0100_2_L.jpg b/unpublishedScripts/marketplace/gameTable/assets/cards/Leather0100_2_L.jpg
deleted file mode 100644
index bf7efd26a3..0000000000
Binary files a/unpublishedScripts/marketplace/gameTable/assets/cards/Leather0100_2_L.jpg and /dev/null differ
diff --git a/unpublishedScripts/marketplace/gameTable/assets/cards/texture_accreditation.txt b/unpublishedScripts/marketplace/gameTable/assets/cards/texture_accreditation.txt
deleted file mode 100644
index da1ed56133..0000000000
--- a/unpublishedScripts/marketplace/gameTable/assets/cards/texture_accreditation.txt
+++ /dev/null
@@ -1 +0,0 @@
-One or more textures on this 3D model have been created with images from CGTextures.com. These images may not be redistributed by default, please visit www.cgtextures.com for more information.
\ No newline at end of file
diff --git a/winprepareVS22 b/winprepareVS22
new file mode 100644
index 0000000000..0a5a19cc73
--- /dev/null
+++ b/winprepareVS22
@@ -0,0 +1,5 @@
+mkdir build
+cd build
+cmake .. -G "Visual Studio 17 2022" -A x64
+ECHO CMake has finished.
+pause