From c04c0375889df0f47074e6b382ca384ab8918864 Mon Sep 17 00:00:00 2001
From: HifiExperiments <thingsandstuffblog@gmail.com>
Date: Fri, 2 Feb 2024 17:59:32 -0800
Subject: [PATCH] native MToon materials

---
 .../src/RenderableEntityItem.cpp              |   49 +-
 .../src/RenderableEntityItem.h                |    5 +
 .../src/RenderableImageEntityItem.cpp         |    3 +-
 .../src/RenderableMaterialEntityItem.cpp      |   34 +-
 .../src/RenderableMaterialEntityItem.h        |    2 +
 .../src/RenderableShapeEntityItem.cpp         |    5 +-
 .../src/RenderableTextEntityItem.cpp          |    3 +-
 .../src/graphics-scripting/Forward.h          |   22 +-
 .../GraphicsScriptingInterface.cpp            |  353 +++---
 .../graphics-scripting/ScriptableModel.cpp    |  167 ++-
 libraries/graphics/src/graphics/Material.cpp  |   42 +-
 libraries/graphics/src/graphics/Material.h    |  125 +-
 libraries/graphics/src/graphics/Material.slh  |  126 +-
 .../src/graphics/MaterialTextures.slh         |  366 ++++--
 .../graphics/src/graphics/ShaderConstants.h   |   16 +-
 .../procedural/ProceduralMaterialCache.cpp    |  674 +++++++---
 .../src/procedural/ProceduralMaterialCache.h  |  121 +-
 .../src/procedural/ReferenceMaterial.cpp      |  119 +-
 .../src/procedural/ReferenceMaterial.h        |   19 +
 libraries/render-utils/src/GlobalLight.slh    |   74 ++
 .../render-utils/src/HighlightEffect.cpp      |  135 +-
 libraries/render-utils/src/HighlightEffect.h  |   35 +-
 .../render-utils/src/MeshPartPayload.cpp      |   31 +-
 libraries/render-utils/src/MeshPartPayload.h  |    4 +
 .../render-utils/src/RenderPipelines.cpp      | 1085 ++++++++++++-----
 .../render-utils/src/RenderShadowTask.cpp     |   55 +-
 libraries/render-utils/src/model.slf          |  156 ++-
 libraries/render-utils/src/model.slv          |    7 +-
 .../render-utils/src/render-utils/model.slp   |    2 +-
 libraries/render/src/render/HighlightStyle.h  |   54 +-
 libraries/render/src/render/Item.cpp          |   10 +-
 libraries/render/src/render/Item.h            |   32 +-
 .../src/render/RenderFetchCullSortTask.cpp    |    9 +-
 .../src/render/RenderFetchCullSortTask.h      |    2 +
 libraries/render/src/render/Scene.cpp         |   17 +
 libraries/render/src/render/Scene.h           |   11 +
 libraries/render/src/render/Selection.h       |    4 +-
 libraries/render/src/render/ShapePipeline.h   |    8 +
 38 files changed, 2992 insertions(+), 990 deletions(-)

diff --git a/libraries/entities-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp
index 212baa6634..db8a61312a 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
@@ -196,6 +197,8 @@ ItemKey EntityRenderer::getKey() {
         builder.withInvisible();
     }
 
+    updateItemKeyBuilderFromMaterials(builder);
+
     return builder;
 }
 
@@ -221,6 +224,20 @@ bool EntityRenderer::passesZoneOcclusionTest(const std::unordered_set<QUuid>& co
     return true;
 }
 
+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;
@@ -510,7 +527,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;
     }
 
@@ -630,6 +647,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;
     {
@@ -656,7 +693,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();
     }
 
@@ -666,8 +703,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 3caeef0713..86ef9dfb54 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"
@@ -74,6 +77,7 @@ public:
     virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) const override;
     virtual Item::Bound getBound(RenderArgs* args) override;
     bool passesZoneOcclusionTest(const std::unordered_set<QUuid>& containingZones) const override;
+    virtual HighlightStyle getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const override;
 
 protected:
     virtual bool needsRenderUpdateFromEntity() const final { return needsRenderUpdateFromEntity(_entity); }
@@ -131,6 +135,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 9592a3e57f..13ddcffe6f 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 b8f829f4ba..b086f42d72 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);
diff --git a/libraries/entities-renderer/src/RenderableMaterialEntityItem.h b/libraries/entities-renderer/src/RenderableMaterialEntityItem.h
index 25403e8a2b..ad1f9771a5 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;
diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
index a6fee03311..1355885625 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) {
@@ -178,7 +177,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/RenderableTextEntityItem.cpp b/libraries/entities-renderer/src/RenderableTextEntityItem.cpp
index 2858e12afd..5b790d6e60 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/graphics-scripting/src/graphics-scripting/Forward.h b/libraries/graphics-scripting/src/graphics-scripting/Forward.h
index 847937ba4f..d120e15892 100644
--- a/libraries/graphics-scripting/src/graphics-scripting/Forward.h
+++ b/libraries/graphics-scripting/src/graphics-scripting/Forward.h
@@ -82,13 +82,33 @@ namespace scriptable {
 
         QString procedural;
 
+        glm::vec3 shade;
+        QString shadeMap;
+        float shadingShift;
+        QString shadingShiftMap;
+        float shadingToony;
+        glm::vec3 matcap;
+        QString matcapMap;
+        glm::vec3 parametricRim;
+        float parametricRimFresnelPower;
+        float parametricRimLift;
+        QString rimMap;
+        float rimLightingMix;
+        QString outlineWidthMode;
+        float outlineWidth;
+        glm::vec3 outline;
+        QString uvAnimationMaskMap;
+        float uvAnimationScrollXSpeed;
+        float uvAnimationScrollYSpeed;
+        float uvAnimationRotationSpeed;
+
         graphics::MaterialKey key { 0 };
     };
 
     /*@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..b44aa9149a 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,179 @@ 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
+                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));
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) {
+                    obj.setProperty("shadeMap", FALLTHROUGH);
+                } else if (!material.shadeMap.isEmpty()) {
+                    obj.setProperty("shadeMap", material.shadeMap);
+                }
+
+                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);
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) {
+                    obj.setProperty("shadingShiftMap", FALLTHROUGH);
+                } else if (!material.shadingShiftMap.isEmpty()) {
+                    obj.setProperty("shadingShiftMap", material.shadingShiftMap);
+                }
+
+                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);
+                }
+
+                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));
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT)) {
+                    obj.setProperty("matcapMap", FALLTHROUGH);
+                } else if (!material.matcapMap.isEmpty()) {
+                    obj.setProperty("matcapMap", material.matcapMap);
+                }
+
+                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));
+                }
+
+                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);
+                }
+
+                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);
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT)) {
+                    obj.setProperty("rimMap", FALLTHROUGH);
+                } else if (!material.rimMap.isEmpty()) {
+                    obj.setProperty("rimMap", material.rimMap);
+                }
+
+                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);
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::LIGHT_MAP_BIT)) {
+                    obj.setProperty("uvAnimationMaskMap", FALLTHROUGH);
+                } else if (!material.uvAnimationMaskMap.isEmpty()) {
+                    obj.setProperty("uvAnimationMaskMap", material.uvAnimationMaskMap);
+                }
+
+                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);
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::EXTRA_1_BIT)) {
+                    obj.setProperty("outlineWidthMode", FALLTHROUGH);
+                } else {
+                    obj.setProperty("outlineWidthMode", material.outlineWidthMode);
+                }
+
+                if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::EXTRA_2_BIT)) {
+                    obj.setProperty("outlineWidth", FALLTHROUGH);
+                } else {
+                    obj.setProperty("outlineWidth", material.outlineWidth);
+                }
+
+                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/Material.cpp b/libraries/graphics/src/graphics/Material.cpp
index 836487de14..0e0ec3588c 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++) {
@@ -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..8c67fc19a6 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,15 +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 };
 
 private:
     std::string _model { HIFI_PBR };
-    mutable MaterialKey _key { 0 };
 
     // Material properties
     glm::vec3 _emissive { DEFAULT_EMISSIVE };
@@ -525,12 +565,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 +578,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 +659,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 +688,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 3a614d26cd..237c780a60 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
@@ -28,6 +29,13 @@
 #define GRAPHICS_TEXTURE_MATERIAL_OCCLUSION 5
 #define GRAPHICS_TEXTURE_MATERIAL_SCATTERING 6
 
+// 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
@@ -59,7 +67,13 @@ enum Texture {
     MaterialRoughness = GRAPHICS_TEXTURE_MATERIAL_ROUGHNESS,
     MaterialOcclusion = GRAPHICS_TEXTURE_MATERIAL_OCCLUSION,
     MaterialScattering = GRAPHICS_TEXTURE_MATERIAL_SCATTERING,
-    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/procedural/src/procedural/ProceduralMaterialCache.cpp b/libraries/procedural/src/procedural/ProceduralMaterialCache.cpp
index f175a65452..047c69285f 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,295 @@ 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)) {
+                                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
@@ -893,3 +1126,138 @@ bool NetworkMaterial::checkResetOpacityMap() {
     }
     return false;
 }
+
+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)
+{}
+
+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..74892b7fd3 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
@@ -63,13 +64,14 @@ protected:
 
     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);
 
+private:
+    // Helpers for the ctors
+    QUrl getTextureUrl(const QUrl& baseUrl, const HFMTexture& hfmTexture);
+
     Transform _albedoTransform;
     Transform _lightmapTransform;
     vec2 _lightmapParams;
@@ -77,6 +79,119 @@ private:
     bool _isOriginal { true };
 };
 
+class NetworkMToonMaterial : public NetworkMaterial {
+public:
+    NetworkMToonMaterial() : NetworkMaterial() {}
+    NetworkMToonMaterial(const NetworkMToonMaterial& material);
+
+    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 {
+        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 {
 public:
     NetworkMaterialResource() : 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/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 5a8b09b018..2152219e77 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);
@@ -518,16 +486,21 @@ void DrawHighlightTask::build(JobModel& task, const render::Varying& inputs, ren
     }
     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
@@ -557,6 +530,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);
@@ -576,3 +553,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 8ba5be54e6..f6e4b3b460 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
@@ -220,6 +221,10 @@ void ModelMeshPartPayload::updateKey(const render::ItemKey& key) {
         builder.withSubMetaCulled();
     }
 
+    if (_drawMaterials.hasOutline()) {
+        builder.withOutline();
+    }
+
     _itemKey = builder.build();
 }
 
@@ -268,11 +273,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());
@@ -377,6 +386,11 @@ bool ModelMeshPartPayload::passesZoneOcclusionTest(const std::unordered_set<QUui
     return true;
 }
 
+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);
@@ -426,4 +440,11 @@ template <> bool payloadPassesZoneOcclusionTest(const ModelMeshPartPayload::Poin
     }
     return false;
 }
+
+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 1a3a898582..b502865f94 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:
@@ -59,6 +61,7 @@ public:
     void setRenderWithZones(const QVector<QUuid>& renderWithZones) { _renderWithZones = renderWithZones; }
     void setBillboardMode(BillboardMode billboardMode) { _billboardMode = billboardMode; }
     bool passesZoneOcclusionTest(const std::unordered_set<QUuid>& containingZones) 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); }
@@ -107,6 +110,7 @@ namespace render {
     template <> const ShapeKey shapeGetShapeKey(const ModelMeshPartPayload::Pointer& payload);
     template <> void payloadRender(const ModelMeshPartPayload::Pointer& payload, RenderArgs* args);
     template <> bool payloadPassesZoneOcclusionTest(const ModelMeshPartPayload::Pointer& payload, const std::unordered_set<QUuid>& containingZones);
+    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/RenderPipelines.cpp b/libraries/render-utils/src/RenderPipelines.cpp
index ed9fb326c4..ccbbb4d66e 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
@@ -42,6 +43,7 @@ namespace gr {
 void initDeferredPipelines(ShapePlumber& plumber, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter);
 void initForwardPipelines(ShapePlumber& plumber);
 void initZPassPipelines(ShapePlumber& plumber, gpu::StatePointer state, const render::ShapePipeline::BatchSetter& batchSetter, const render::ShapePipeline::ItemSetter& itemSetter);
+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,
@@ -84,6 +86,11 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip
         { 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 MToon
+        { Key::Builder().withMaterial().withMToon(), model_mtoon },
+        { Key::Builder().withMaterial().withTangents().withMToon(), model_normalmap_mtoon },
+        { Key::Builder().withMaterial().withTranslucent().withMToon(), model_translucent_mtoon },
+        { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon(), model_normalmap_translucent_mtoon },
         // Unskinned Fade
         { Key::Builder().withMaterial().withFade(), model_fade },
         { Key::Builder().withMaterial().withTangents().withFade(), model_normalmap_fade },
@@ -99,6 +106,11 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip
         { 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 },
+        // Unskinned MToon Fade
+        { Key::Builder().withMaterial().withMToon().withFade(), model_mtoon_fade },
+        { Key::Builder().withMaterial().withTangents().withMToon().withFade(), model_normalmap_mtoon_fade },
+        { Key::Builder().withMaterial().withTranslucent().withMToon().withFade(), model_translucent_mtoon_fade },
+        { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon().withFade(), model_normalmap_translucent_mtoon_fade },
 
         // Matrix palette skinned
         { Key::Builder().withMaterial().withDeformed(), model_deformed },
@@ -115,6 +127,11 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip
         { 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 MToon
+        { Key::Builder().withMaterial().withMToon().withDeformed(), model_mtoon_deformed },
+        { Key::Builder().withMaterial().withTangents().withMToon().withDeformed(), model_normalmap_mtoon_deformed },
+        { Key::Builder().withMaterial().withTranslucent().withMToon().withDeformed(), model_translucent_mtoon_deformed },
+        { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon().withDeformed(), model_normalmap_translucent_mtoon_deformed },
         // Matrix palette skinned Fade
         { Key::Builder().withMaterial().withFade().withDeformed(), model_fade_deformed },
         { Key::Builder().withMaterial().withTangents().withFade().withDeformed(), model_normalmap_fade_deformed },
@@ -130,6 +147,11 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip
         { 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 },
+        // Matrix palette skinned MToon Fade
+        { Key::Builder().withMaterial().withMToon().withFade().withDeformed(), model_mtoon_fade_deformed },
+        { Key::Builder().withMaterial().withTangents().withMToon().withFade().withDeformed(), model_normalmap_mtoon_fade_deformed },
+        { Key::Builder().withMaterial().withTranslucent().withMToon().withFade().withDeformed(), model_translucent_mtoon_fade_deformed },
+        { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon().withFade().withDeformed(), model_normalmap_translucent_mtoon_fade_deformed },
 
         // Dual quaternion skinned
         { Key::Builder().withMaterial().withDeformed().withDualQuatSkinned(), model_deformeddq },
@@ -146,6 +168,11 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip
         { 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 MToon
+        { Key::Builder().withMaterial().withMToon().withDeformed().withDualQuatSkinned(), model_mtoon_deformeddq },
+        { Key::Builder().withMaterial().withTangents().withMToon().withDeformed().withDualQuatSkinned(), model_normalmap_mtoon_deformeddq },
+        { Key::Builder().withMaterial().withTranslucent().withMToon().withDeformed().withDualQuatSkinned(), model_translucent_mtoon_deformeddq },
+        { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_mtoon_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 },
@@ -161,6 +188,11 @@ void initDeferredPipelines(render::ShapePlumber& plumber, const render::ShapePip
         { 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 },
+        // Dual quaternion skinned MToon Fade
+        { Key::Builder().withMaterial().withMToon().withFade().withDeformed().withDualQuatSkinned(), model_mtoon_fade_deformeddq },
+        { Key::Builder().withMaterial().withTangents().withMToon().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_mtoon_fade_deformeddq },
+        { Key::Builder().withMaterial().withTranslucent().withMToon().withFade().withDeformed().withDualQuatSkinned(), model_translucent_mtoon_fade_deformeddq },
+        { Key::Builder().withMaterial().withTangents().withTranslucent().withMToon().withFade().withDeformed().withDualQuatSkinned(), model_normalmap_translucent_mtoon_fade_deformeddq },
     };
 
     for (auto& pipeline : pipelines) {
@@ -209,6 +241,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 },
@@ -225,6 +262,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 },
@@ -241,6 +283,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) {
@@ -286,7 +333,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) {
@@ -347,25 +394,97 @@ void initZPassPipelines(ShapePlumber& shapePlumber, gpu::StatePointer state, con
     using namespace shader::render_utils::program;
 
     shapePlumber.addPipeline(
-        ShapeKey::Filter::Builder().withoutDeformed().withoutFade(),
+        ShapeKey::Filter::Builder().withoutMToon().withoutDeformed().withoutFade(),
         gpu::Shader::createProgram(model_shadow), state);
     shapePlumber.addPipeline(
-        ShapeKey::Filter::Builder().withoutDeformed().withFade(),
+        ShapeKey::Filter::Builder().withoutMToon().withoutDeformed().withFade(),
         gpu::Shader::createProgram(model_shadow_fade), state, extraBatchSetter, itemSetter);
+    shapePlumber.addPipeline(
+        ShapeKey::Filter::Builder().withMToon().withoutDeformed().withoutFade(),
+        gpu::Shader::createProgram(model_shadow_mtoon), state);
+    shapePlumber.addPipeline(
+        ShapeKey::Filter::Builder().withMToon().withoutDeformed().withFade(),
+        gpu::Shader::createProgram(model_shadow_mtoon_fade), state, extraBatchSetter, itemSetter);
 
     shapePlumber.addPipeline(
-        ShapeKey::Filter::Builder().withDeformed().withoutDualQuatSkinned().withoutFade(),
+        ShapeKey::Filter::Builder().withoutMToon().withDeformed().withoutDualQuatSkinned().withoutFade(),
         gpu::Shader::createProgram(model_shadow_deformed), state);
     shapePlumber.addPipeline(
-        ShapeKey::Filter::Builder().withDeformed().withoutDualQuatSkinned().withFade(),
+        ShapeKey::Filter::Builder().withoutMToon().withDeformed().withoutDualQuatSkinned().withFade(),
         gpu::Shader::createProgram(model_shadow_fade_deformed), state, extraBatchSetter, itemSetter);
+    shapePlumber.addPipeline(
+        ShapeKey::Filter::Builder().withMToon().withDeformed().withoutDualQuatSkinned().withoutFade(),
+        gpu::Shader::createProgram(model_shadow_mtoon_deformed), state);
+    shapePlumber.addPipeline(
+        ShapeKey::Filter::Builder().withMToon().withDeformed().withoutDualQuatSkinned().withFade(),
+        gpu::Shader::createProgram(model_shadow_mtoon_fade_deformed), state, extraBatchSetter, itemSetter);
 
     shapePlumber.addPipeline(
-        ShapeKey::Filter::Builder().withDeformed().withDualQuatSkinned().withoutFade(),
+        ShapeKey::Filter::Builder().withoutMToon().withDeformed().withDualQuatSkinned().withoutFade(),
         gpu::Shader::createProgram(model_shadow_deformeddq), state);
     shapePlumber.addPipeline(
-        ShapeKey::Filter::Builder().withDeformed().withDualQuatSkinned().withFade(),
+        ShapeKey::Filter::Builder().withoutMToon().withDeformed().withDualQuatSkinned().withFade(),
         gpu::Shader::createProgram(model_shadow_fade_deformeddq), state, extraBatchSetter, itemSetter);
+    shapePlumber.addPipeline(
+        ShapeKey::Filter::Builder().withMToon().withDeformed().withDualQuatSkinned().withoutFade(),
+        gpu::Shader::createProgram(model_shadow_mtoon_deformeddq), state);
+    shapePlumber.addPipeline(
+        ShapeKey::Filter::Builder().withMToon().withDeformed().withDualQuatSkinned().withFade(),
+        gpu::Shader::createProgram(model_shadow_mtoon_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<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.hasOwnPipeline()) {
+            sortedOwnPipelineShapeKeys[variantKey.build()].push_back(items.first);
+        } else {
+            sortedShapeKeys[variantKey.build()].push_back(items.first);
+        }
+    }
+
+    // Render non-withOwnPipeline things
+    for (auto& variantAndKeys : sortedShapeKeys) {
+        if (variantAndKeys.second.size() > 0) {
+            for (const auto& key : variantAndKeys.second) {
+                renderShapes(renderContext, shapePlumber, inShapes.at(key));
+            }
+        }
+    }
+
+    // Render withOwnPipeline things
+    for (auto& variantAndKeys : sortedOwnPipelineShapeKeys) {
+        if (variantAndKeys.second.size() > 0) {
+            for (const auto& key : variantAndKeys.second) {
+                renderShapes(renderContext, shapePlumber, inShapes.at(key));
+            }
+        }
+    }
+
+    renderContext->args->_shapePipeline = nullptr;
+    renderContext->args->_batch = nullptr;
 }
 
 bool RenderPipelines::bindMaterial(graphics::MaterialPointer& material, gpu::Batch& batch, render::Args::RenderMode renderMode, bool enableTextures) {
@@ -375,11 +494,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;
@@ -398,6 +517,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;
@@ -425,261 +545,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) {
@@ -704,71 +1133,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();
 }
@@ -778,10 +1251,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] {
@@ -795,6 +1274,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
@@ -805,20 +1296,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 dbfa23a143..fa2c6091dd 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());
@@ -253,58 +254,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 98abc29d8c..4b40aa171c 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,45 @@
 <@include render-utils/ShaderConstants.h@>
 <@include CullFace.slh@>
 
+<@if HIFI_USE_MTOON@>
+    <@if HIFI_USE_SHADOW@>
+        <$declareMToonMaterialTextures(ALBEDO)$>
+    <@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)$>
+    <@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 +69,7 @@
         <@endif@>
         <@include gpu/Transform.slh@>
         <$declareStandardCameraTransform()$>
+
         layout(location=0) out vec4 _fragColor0;
     <@else@>
         <@include DeferredBufferWrite.slh@>
@@ -43,29 +82,6 @@
     <@include LightingModel.slh@>
 <@endif@>
 
-<@if HIFI_USE_SHADOW or HIFI_USE_UNLIT@>
-    <$declareMaterialTextures(ALBEDO)$>
-<@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()$>
@@ -78,7 +94,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@>
@@ -101,15 +119,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)$>;
@@ -118,7 +142,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@>
@@ -134,6 +160,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@>
@@ -154,14 +247,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 b63ec898eb..e69518f8fd 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 fade:f/forward:f deformed:v/deformeddq:v
\ No newline at end of file
+DEFINES (normalmap translucent:f unlit:f/lightmap:f)/shadow mtoon fade:f/forward:f deformed:v/deformeddq:v
\ No newline at end of file
diff --git a/libraries/render/src/render/HighlightStyle.h b/libraries/render/src/render/HighlightStyle.h
index 8bef5c33c3..795253cc2f 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 = 0.5 * screenHeight * fabs(p1.y - p2.y);
+            } else { // OUTLINE_SCREEN
+                style._outlineWidth = outlineWidth * screenHeight;
+            }
+
+            return style;
+        }
     };
 
 }
diff --git a/libraries/render/src/render/Item.cpp b/libraries/render/src/render/Item.cpp
index 369f227566..62b3b20523 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
@@ -160,4 +161,11 @@ namespace render {
         }
         return payload->passesZoneOcclusionTest(containingZones);
     }
-}
+
+    template <> HighlightStyle payloadGetOutlineStyle(const PayloadProxyInterface::Pointer& payload, const ViewFrustum& viewFrustum, const size_t height) {
+        if (!payload) {
+            return HighlightStyle();
+        }
+        return payload->getOutlineStyle(viewFrustum, height);
+    }
+}
\ No newline at end of file
diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h
index 5952be8a84..25663899d1 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,15 +106,17 @@ 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,
 
+        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;
 
@@ -170,6 +174,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(); }
@@ -213,6 +220,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)); }
@@ -284,6 +293,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
@@ -440,6 +452,8 @@ public:
 
         virtual bool passesZoneOcclusionTest(const std::unordered_set<QUuid>& containingZones) const = 0;
 
+        virtual HighlightStyle getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const = 0;
+
         ~PayloadInterface() {}
 
         // Status interface is local to the base class
@@ -493,6 +507,8 @@ public:
 
     bool passesZoneOcclusionTest(const std::unordered_set<QUuid>& containingZones) const { return _payload->passesZoneOcclusionTest(containingZones); }
 
+    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(); }
 
@@ -547,6 +563,12 @@ template <class T> uint32_t metaFetchMetaSubItems(const std::shared_ptr<T>& payl
 // Allows payloads to determine if they should render or not, based on the zones that contain the current camera
 template <class T> bool payloadPassesZoneOcclusionTest(const std::shared_ptr<T>& payloadData, const std::unordered_set<QUuid>& containingZones) { return true; }
 
+// 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"
@@ -573,6 +595,8 @@ public:
 
     virtual bool passesZoneOcclusionTest(const std::unordered_set<QUuid>& containingZones) const override { return payloadPassesZoneOcclusionTest<T>(_data, containingZones); }
 
+    virtual HighlightStyle getOutlineStyle(const ViewFrustum& viewFrustum, const size_t height) const override { return payloadGetOutlineStyle<T>(_data, viewFrustum, height); }
+
 protected:
     DataPointer _data;
 
@@ -628,6 +652,7 @@ public:
     virtual void render(RenderArgs* args) = 0;
     virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) const = 0;
     virtual bool passesZoneOcclusionTest(const std::unordered_set<QUuid>& containingZones) 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,
@@ -640,6 +665,7 @@ template <> void payloadRender(const PayloadProxyInterface::Pointer& payload, Re
 template <> uint32_t metaFetchMetaSubItems(const PayloadProxyInterface::Pointer& payload, ItemIDs& subItems);
 template <> const ShapeKey shapeGetShapeKey(const PayloadProxyInterface::Pointer& payload);
 template <> bool payloadPassesZoneOcclusionTest(const PayloadProxyInterface::Pointer& payload, const std::unordered_set<QUuid>& containingZones);
+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 b2656a597f..f30fbc155e 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,18 +36,20 @@ 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 = 4; 
+    const int NUM_SPATIAL_FILTERS = 5;
     const int NUM_NON_SPATIAL_FILTERS = 3;
     const int OPAQUE_SHAPE_BUCKET = 0;
     const int TRANSPARENT_SHAPE_BUCKET = 1;
     const int LIGHT_BUCKET = 2;
     const int META_BUCKET = 3;
+    const int OUTLINE_BUCKET = 4;
     const int BACKGROUND_BUCKET = 2;
     MultiFilterItems<NUM_SPATIAL_FILTERS>::ItemFilterArray spatialFilters = { {
             ItemFilter::Builder::opaqueShape(),
             ItemFilter::Builder::transparentShape(),
             ItemFilter::Builder::light(),
-            ItemFilter::Builder::meta()
+            ItemFilter::Builder::meta(),
+            ItemFilter::Builder().withVisible().withOutline()
         } };
     MultiFilterItems<NUM_NON_SPATIAL_FILTERS>::ItemFilterArray nonspatialFilters = { {
             ItemFilter::Builder::opaqueShape(),
@@ -76,7 +79,7 @@ void RenderFetchCullSortTask::build(JobModel& task, const Varying& input, Varyin
 
     task.addJob<ClearContainingZones>("ClearContainingZones");
 
-    output = Output(BucketList{ opaques, transparents, lights, metas,
+    output = Output(BucketList{ opaques, transparents, lights, metas, 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 0b475614a1..bb01a81d81 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
@@ -23,6 +24,7 @@ public:
         TRANSPARENT_SHAPE,
         LIGHT,
         META,
+        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 1bc282646b..19d56b61d2 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
@@ -194,6 +203,8 @@ public:
     void setItemTransition(ItemID id, Index transitionId);
     void removeItemTransition(ItemID id);
 
+    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.h b/libraries/render/src/render/ShapePipeline.h
index 04b9919140..8e3505b2e5 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,
@@ -84,6 +86,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) {
@@ -184,6 +187,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); }
 
@@ -211,6 +217,7 @@ public:
     bool isWireframe() const { return _flags[WIREFRAME]; }
     bool isCullFace() 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 +257,7 @@ inline QDebug operator<<(QDebug debug, const ShapeKey& key) {
                 << "isWireframe:" << key.isWireframe()
                 << "isCullFace:" << key.isCullFace()
                 << "isFaded:" << key.isFaded()
+                << "isMToon:" << key.isMToon()
                 << "]";
         }
     } else {