From d6841c07983a46ff805ed23a7318efbf9cca3b24 Mon Sep 17 00:00:00 2001
From: Cosmic Linden <cosmic@lindenlab.com>
Date: Thu, 9 Feb 2023 15:04:46 -0800
Subject: [PATCH] SL-19080: Update GLTF Material asset upload to v1.1, with
 stricter GLTF compliance and removal of unsupported features

---
 indra/llprimitive/CMakeLists.txt              |   2 +
 indra/llprimitive/llgltfmaterial.cpp          | 151 ++++++-----
 indra/llprimitive/llgltfmaterial.h            |  53 ++--
 .../llprimitive/tests/llgltfmaterial_test.cpp | 255 ++++++++++++++++++
 indra/newview/llgltfmateriallist.cpp          |   4 +-
 indra/newview/llmaterialeditor.cpp            | 209 ++++----------
 indra/newview/llmaterialeditor.h              |   9 +-
 indra/newview/lltinygltfhelper.cpp            |  16 +-
 indra/newview/llviewermenufile.cpp            |   8 +-
 indra/newview/llviewerobject.cpp              |   8 +-
 10 files changed, 441 insertions(+), 274 deletions(-)
 create mode 100644 indra/llprimitive/tests/llgltfmaterial_test.cpp

diff --git a/indra/llprimitive/CMakeLists.txt b/indra/llprimitive/CMakeLists.txt
index 328b22f900b..dd25c197132 100644
--- a/indra/llprimitive/CMakeLists.txt
+++ b/indra/llprimitive/CMakeLists.txt
@@ -99,6 +99,8 @@ if (LL_TESTS)
     SET(llprimitive_TEST_SOURCE_FILES
       llmediaentry.cpp
       llprimitive.cpp
+      llgltfmaterial.cpp
       )
+    
     LL_ADD_PROJECT_UNIT_TESTS(llprimitive "${llprimitive_TEST_SOURCE_FILES}")
 endif (LL_TESTS)
diff --git a/indra/llprimitive/llgltfmaterial.cpp b/indra/llprimitive/llgltfmaterial.cpp
index a8dad89292c..2e920aa44e1 100644
--- a/indra/llprimitive/llgltfmaterial.cpp
+++ b/indra/llprimitive/llgltfmaterial.cpp
@@ -31,6 +31,10 @@
 // NOTE -- this should be the one and only place tiny_gltf.h is included
 #include "tinygltf/tiny_gltf.h"
 
+const char* LLGLTFMaterial::ASSET_VERSION = "1.1";
+const char* LLGLTFMaterial::ASSET_TYPE = "GLTF 2.0";
+const std::array<char*, 2> LLGLTFMaterial::ACCEPTED_ASSET_VERSIONS = { "1.0", "1.1" };
+
 const char* GLTF_FILE_EXTENSION_TRANSFORM = "KHR_texture_transform";
 const char* GLTF_FILE_EXTENSION_TRANSFORM_SCALE = "scale";
 const char* GLTF_FILE_EXTENSION_TRANSFORM_OFFSET = "offset";
@@ -73,16 +77,14 @@ LLGLTFMaterial::LLGLTFMaterial(const LLGLTFMaterial& rhs)
 
 LLGLTFMaterial& LLGLTFMaterial::operator=(const LLGLTFMaterial& rhs)
 {
-    LL_PROFILE_ZONE_SCOPED;
-    //have to do a manual operator= because of LLRefCount
-    mBaseColorId = rhs.mBaseColorId;
-    mNormalId = rhs.mNormalId;
-    mMetallicRoughnessId = rhs.mMetallicRoughnessId;
-    mEmissiveId = rhs.mEmissiveId;
+    //have to do a manual operator= because of LLRefCount 
+    mTextureId = rhs.mTextureId;
+
+    mTextureTransform = rhs.mTextureTransform;
 
     mBaseColor = rhs.mBaseColor;
     mEmissiveColor = rhs.mEmissiveColor;
-
+    
     mMetallicFactor = rhs.mMetallicFactor;
     mRoughnessFactor = rhs.mRoughnessFactor;
     mAlphaCutoff = rhs.mAlphaCutoff;
@@ -90,8 +92,6 @@ LLGLTFMaterial& LLGLTFMaterial::operator=(const LLGLTFMaterial& rhs)
     mDoubleSided = rhs.mDoubleSided;
     mAlphaMode = rhs.mAlphaMode;
 
-    mTextureTransform = rhs.mTextureTransform;
-
     mOverrideDoubleSided = rhs.mOverrideDoubleSided;
     mOverrideAlphaMode = rhs.mOverrideAlphaMode;
 
@@ -100,10 +100,9 @@ LLGLTFMaterial& LLGLTFMaterial::operator=(const LLGLTFMaterial& rhs)
 
 bool LLGLTFMaterial::operator==(const LLGLTFMaterial& rhs) const
 {
-    return mBaseColorId == rhs.mBaseColorId &&
-        mNormalId == rhs.mNormalId &&
-        mMetallicRoughnessId == rhs.mMetallicRoughnessId &&
-        mEmissiveId == rhs.mEmissiveId &&
+    return mTextureId == rhs.mTextureId &&
+
+        mTextureTransform == rhs.mTextureTransform &&
 
         mBaseColor == rhs.mBaseColor &&
         mEmissiveColor == rhs.mEmissiveColor &&
@@ -115,8 +114,6 @@ bool LLGLTFMaterial::operator==(const LLGLTFMaterial& rhs) const
         mDoubleSided == rhs.mDoubleSided &&
         mAlphaMode == rhs.mAlphaMode &&
 
-        mTextureTransform == rhs.mTextureTransform &&
-
         mOverrideDoubleSided == rhs.mOverrideDoubleSided &&
         mOverrideAlphaMode == rhs.mOverrideAlphaMode;
 }
@@ -148,6 +145,8 @@ std::string LLGLTFMaterial::asJSON(bool prettyprint) const
 
     writeToModel(model_out, 0);
 
+    // To ensure consistency in asset upload, this should be the only reference
+    // to WriteGltfSceneToStream in the viewer.
     gltf.WriteGltfSceneToStream(&model_out, str, prettyprint, false);
 
     return str.str();
@@ -164,13 +163,13 @@ void LLGLTFMaterial::setFromModel(const tinygltf::Model& model, S32 mat_index)
     const tinygltf::Material& material_in = model.materials[mat_index];
 
     // Apply base color texture
-    setFromTexture(model, material_in.pbrMetallicRoughness.baseColorTexture, GLTF_TEXTURE_INFO_BASE_COLOR, mBaseColorId);
+    setFromTexture(model, material_in.pbrMetallicRoughness.baseColorTexture, GLTF_TEXTURE_INFO_BASE_COLOR);
     // Apply normal map
-    setFromTexture(model, material_in.normalTexture, GLTF_TEXTURE_INFO_NORMAL, mNormalId);
+    setFromTexture(model, material_in.normalTexture, GLTF_TEXTURE_INFO_NORMAL);
     // Apply metallic-roughness texture
-    setFromTexture(model, material_in.pbrMetallicRoughness.metallicRoughnessTexture, GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS, mMetallicRoughnessId);
+    setFromTexture(model, material_in.pbrMetallicRoughness.metallicRoughnessTexture, GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS);
     // Apply emissive texture
-    setFromTexture(model, material_in.emissiveTexture, GLTF_TEXTURE_INFO_EMISSIVE, mEmissiveId);
+    setFromTexture(model, material_in.emissiveTexture, GLTF_TEXTURE_INFO_EMISSIVE);
 
     setAlphaMode(material_in.alphaMode);
     mAlphaCutoff = llclamp((F32)material_in.alphaCutoff, 0.f, 1.f);
@@ -264,11 +263,11 @@ std::string gltf_get_texture_image(const tinygltf::Model& model, const T& textur
 
 // *NOTE: Use template here as workaround for the different similar texture info classes
 template<typename T>
-void LLGLTFMaterial::setFromTexture(const tinygltf::Model& model, const T& texture_info, TextureInfo texture_info_id, LLUUID& texture_id_out)
+void LLGLTFMaterial::setFromTexture(const tinygltf::Model& model, const T& texture_info, TextureInfo texture_info_id)
 {
     LL_PROFILE_ZONE_SCOPED;
     const std::string uri = gltf_get_texture_image(model, texture_info);
-    texture_id_out.set(uri);
+    mTextureId[texture_info_id].set(uri);
 
     const tinygltf::Value::Object& extensions_object = texture_info.extensions;
     const auto transform_it = extensions_object.find(GLTF_FILE_EXTENSION_TRANSFORM);
@@ -297,21 +296,24 @@ void LLGLTFMaterial::writeToModel(tinygltf::Model& model, S32 mat_index) const
     tinygltf::Material& material_out = model.materials[mat_index];
 
     // set base color texture
-    writeToTexture(model, material_out.pbrMetallicRoughness.baseColorTexture, GLTF_TEXTURE_INFO_BASE_COLOR, mBaseColorId);
+    writeToTexture(model, material_out.pbrMetallicRoughness.baseColorTexture, GLTF_TEXTURE_INFO_BASE_COLOR);
     // set normal texture
-    writeToTexture(model, material_out.normalTexture, GLTF_TEXTURE_INFO_NORMAL, mNormalId);
+    writeToTexture(model, material_out.normalTexture, GLTF_TEXTURE_INFO_NORMAL);
     // set metallic-roughness texture
-    writeToTexture(model, material_out.pbrMetallicRoughness.metallicRoughnessTexture, GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS, mMetallicRoughnessId);
+    writeToTexture(model, material_out.pbrMetallicRoughness.metallicRoughnessTexture, GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS);
     // set emissive texture
-    writeToTexture(model, material_out.emissiveTexture, GLTF_TEXTURE_INFO_EMISSIVE, mEmissiveId);
+    writeToTexture(model, material_out.emissiveTexture, GLTF_TEXTURE_INFO_EMISSIVE);
+    // set occlusion texture
+    // *NOTE: This is required for ORM materials for GLTF compliance.
+    // See: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#_material_occlusiontexture
+    writeToTexture(model, material_out.occlusionTexture, GLTF_TEXTURE_INFO_OCCLUSION);
+
 
     material_out.alphaMode = getAlphaMode();
     material_out.alphaCutoff = mAlphaCutoff;
-
+    
     mBaseColor.write(material_out.pbrMetallicRoughness.baseColorFactor);
 
-    material_out.emissiveFactor.resize(3); // 0 size by default
-
     if (mEmissiveColor != LLGLTFMaterial::getDefaultEmissiveColor())
     {
         material_out.emissiveFactor.resize(3);
@@ -323,7 +325,6 @@ void LLGLTFMaterial::writeToModel(tinygltf::Model& model, S32 mat_index) const
 
     material_out.doubleSided = mDoubleSided;
 
-
     // generate "extras" string
     tinygltf::Value::Object extras;
     bool write_extras = false;
@@ -364,28 +365,43 @@ void gltf_allocate_texture_image(tinygltf::Model& model, T& texture_info, const
 }
 
 template<typename T>
-void LLGLTFMaterial::writeToTexture(tinygltf::Model& model, T& texture_info, TextureInfo texture_info_id, const LLUUID& texture_id) const
+void LLGLTFMaterial::writeToTexture(tinygltf::Model& model, T& texture_info, TextureInfo texture_info_id, bool force_write) const
 {
     LL_PROFILE_ZONE_SCOPED;
+    const LLUUID& texture_id = mTextureId[texture_info_id];
     const TextureTransform& transform = mTextureTransform[texture_info_id];
-    if (texture_id.isNull() && transform == sDefault.mTextureTransform[0])
+    const bool is_blank_transform = transform == sDefault.mTextureTransform[0];
+    // Check if this material matches all the fallback values, and if so, then
+    // skip including it to reduce material size
+    if (!force_write && texture_id.isNull() && is_blank_transform)
     {
         return;
     }
 
+    // tinygltf will discard this texture info if there is no valid texture,
+    // causing potential loss of information for overrides, so ensure one is
+    // defined. -Cosmic,2023-01-30
     gltf_allocate_texture_image(model, texture_info, texture_id.asString());
 
-    tinygltf::Value::Object transform_map;
-    transform_map[GLTF_FILE_EXTENSION_TRANSFORM_OFFSET] = tinygltf::Value(tinygltf::Value::Array({
-        tinygltf::Value(transform.mOffset.mV[VX]),
-        tinygltf::Value(transform.mOffset.mV[VY])
-    }));
-    transform_map[GLTF_FILE_EXTENSION_TRANSFORM_SCALE] = tinygltf::Value(tinygltf::Value::Array({
-        tinygltf::Value(transform.mScale.mV[VX]),
-        tinygltf::Value(transform.mScale.mV[VY])
-    }));
-    transform_map[GLTF_FILE_EXTENSION_TRANSFORM_ROTATION] = tinygltf::Value(transform.mRotation);
-    texture_info.extensions[GLTF_FILE_EXTENSION_TRANSFORM] = tinygltf::Value(transform_map);
+    if (!is_blank_transform)
+    {
+        tinygltf::Value::Object transform_map;
+        transform_map[GLTF_FILE_EXTENSION_TRANSFORM_OFFSET] = tinygltf::Value(tinygltf::Value::Array({
+            tinygltf::Value(transform.mOffset.mV[VX]),
+            tinygltf::Value(transform.mOffset.mV[VY])
+        }));
+        transform_map[GLTF_FILE_EXTENSION_TRANSFORM_SCALE] = tinygltf::Value(tinygltf::Value::Array({
+            tinygltf::Value(transform.mScale.mV[VX]),
+            tinygltf::Value(transform.mScale.mV[VY])
+        }));
+        transform_map[GLTF_FILE_EXTENSION_TRANSFORM_ROTATION] = tinygltf::Value(transform.mRotation);
+        texture_info.extensions[GLTF_FILE_EXTENSION_TRANSFORM] = tinygltf::Value(transform_map);
+    }
+}
+
+void LLGLTFMaterial::sanitizeAssetMaterial()
+{
+    mTextureTransform = sDefault.mTextureTransform;
 }
 
 bool LLGLTFMaterial::setBaseMaterial()
@@ -419,40 +435,33 @@ void LLGLTFMaterial::hackOverrideUUID(LLUUID& id)
     }
 }
 
-void LLGLTFMaterial::setBaseColorId(const LLUUID& id, bool for_override)
+void LLGLTFMaterial::setTextureId(TextureInfo texture_info, const LLUUID& id, bool for_override)
 {
-    mBaseColorId = id;
+    mTextureId[texture_info] = id;
     if (for_override)
     {
-        hackOverrideUUID(mBaseColorId);
+        hackOverrideUUID(mTextureId[texture_info]);
     }
 }
 
+void LLGLTFMaterial::setBaseColorId(const LLUUID& id, bool for_override)
+{
+    setTextureId(GLTF_TEXTURE_INFO_BASE_COLOR, id, for_override);
+}
+
 void LLGLTFMaterial::setNormalId(const LLUUID& id, bool for_override)
 {
-    mNormalId = id;
-    if (for_override)
-    {
-        hackOverrideUUID(mNormalId);
-    }
+    setTextureId(GLTF_TEXTURE_INFO_NORMAL, id, for_override);
 }
 
-void LLGLTFMaterial::setMetallicRoughnessId(const LLUUID& id, bool for_override)
+void LLGLTFMaterial::setOcclusionRoughnessMetallicId(const LLUUID& id, bool for_override)
 {
-    mMetallicRoughnessId = id;
-    if (for_override)
-    {
-        hackOverrideUUID(mMetallicRoughnessId);
-    }
+    setTextureId(GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS, id, for_override);
 }
 
 void LLGLTFMaterial::setEmissiveId(const LLUUID& id, bool for_override)
 {
-    mEmissiveId = id;
-    if (for_override)
-    {
-        hackOverrideUUID(mEmissiveId);
-    }
+    setTextureId(GLTF_TEXTURE_INFO_EMISSIVE, id, for_override);
 }
 
 void LLGLTFMaterial::setBaseColorFactor(const LLColor4& baseColor, bool for_override)
@@ -533,10 +542,7 @@ const char* LLGLTFMaterial::getAlphaMode() const
 void LLGLTFMaterial::setAlphaMode(S32 mode, bool for_override)
 {
     mAlphaMode = (AlphaMode) llclamp(mode, (S32) ALPHA_MODE_OPAQUE, (S32) ALPHA_MODE_MASK);
-    if (for_override)
-    {
-        mOverrideAlphaMode = true;
-    }
+    mOverrideAlphaMode = for_override && mAlphaMode == getDefaultAlphaMode();
 }
 
 void LLGLTFMaterial::setDoubleSided(bool double_sided, bool for_override)
@@ -544,10 +550,7 @@ void LLGLTFMaterial::setDoubleSided(bool double_sided, bool for_override)
     // sure, no clamping will ever be needed for a bool, but include the
     // setter for consistency with the clamping API
     mDoubleSided = double_sided;
-    if (for_override)
-    {
-        mOverrideDoubleSided = true;
-    }
+    mOverrideDoubleSided = for_override && mDoubleSided == getDefaultDoubleSided();
 }
 
 void LLGLTFMaterial::setTextureOffset(TextureInfo texture_info, const LLVector2& offset)
@@ -640,10 +643,12 @@ void LLGLTFMaterial::applyOverride(const LLGLTFMaterial& override_mat)
 {
     LL_PROFILE_ZONE_SCOPED;
 
-    applyOverrideUUID(mBaseColorId, override_mat.mBaseColorId);
-    applyOverrideUUID(mNormalId, override_mat.mNormalId);
-    applyOverrideUUID(mMetallicRoughnessId, override_mat.mMetallicRoughnessId);
-    applyOverrideUUID(mEmissiveId, override_mat.mEmissiveId);
+    for (int i = 0; i < GLTF_TEXTURE_INFO_COUNT; ++i)
+    {
+        LLUUID& texture_id = mTextureId[i];
+        const LLUUID& override_texture_id = override_mat.mTextureId[i];
+        applyOverrideUUID(texture_id, override_texture_id);
+    }
 
     if (override_mat.mBaseColor != getDefaultBaseColor())
     {
diff --git a/indra/llprimitive/llgltfmaterial.h b/indra/llprimitive/llgltfmaterial.h
index dec7b696f87..fd26e7563ce 100644
--- a/indra/llprimitive/llgltfmaterial.h
+++ b/indra/llprimitive/llgltfmaterial.h
@@ -49,6 +49,11 @@ class LLGLTFMaterial : public LLRefCount
     // default material for reference
     static const LLGLTFMaterial sDefault;
 
+    static const char* ASSET_VERSION;
+    static const char* ASSET_TYPE;
+    static const std::array<char*, 2> ACCEPTED_ASSET_VERSIONS;
+    static bool isAcceptedVersion(const std::string& version) { return std::find(ACCEPTED_ASSET_VERSIONS.cbegin(), ACCEPTED_ASSET_VERSIONS.cend(), version) != ACCEPTED_ASSET_VERSIONS.cend(); }
+
     struct TextureTransform
     {
         LLVector2 mOffset = { 0.f, 0.f };
@@ -74,10 +79,25 @@ class LLGLTFMaterial : public LLRefCount
     bool operator==(const LLGLTFMaterial& rhs) const;
     bool operator!=(const LLGLTFMaterial& rhs) const { return !(*this == rhs); }
 
-    LLUUID mBaseColorId;
-    LLUUID mNormalId;
-    LLUUID mMetallicRoughnessId;
-    LLUUID mEmissiveId;
+    enum TextureInfo : U32
+    {
+        GLTF_TEXTURE_INFO_BASE_COLOR,
+        GLTF_TEXTURE_INFO_NORMAL,
+        GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS,
+        // *NOTE: GLTF_TEXTURE_INFO_OCCLUSION is currently ignored, in favor of
+        // the values specified with GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS.
+        // Currently, only ORM materials are supported (materials which define
+        // occlusion, roughness, and metallic in the same texture).
+        // -Cosmic,2023-01-26
+        GLTF_TEXTURE_INFO_OCCLUSION = GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS,
+        GLTF_TEXTURE_INFO_EMISSIVE,
+
+        GLTF_TEXTURE_INFO_COUNT
+    };
+
+    std::array<LLUUID, GLTF_TEXTURE_INFO_COUNT> mTextureId;
+
+    std::array<TextureTransform, GLTF_TEXTURE_INFO_COUNT> mTextureTransform;
 
     // NOTE : initialize values to defaults according to the GLTF spec
     LLColor4 mBaseColor = LLColor4(1, 1, 1, 1);
@@ -106,24 +126,14 @@ class LLGLTFMaterial : public LLRefCount
         return id;
     }
 
-    enum TextureInfo : U32
-    {
-        GLTF_TEXTURE_INFO_BASE_COLOR,
-        GLTF_TEXTURE_INFO_NORMAL,
-        GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS,
-        GLTF_TEXTURE_INFO_EMISSIVE,
-
-        GLTF_TEXTURE_INFO_COUNT
-    };
-
-    std::array<TextureTransform, GLTF_TEXTURE_INFO_COUNT> mTextureTransform;
-
     //setters for various members (will clamp to acceptable ranges)
     // for_override - set to true if this value is being set as part of an override (important for handling override to default value)
 
+    void setTextureId(TextureInfo texture_info, const LLUUID& id, bool for_override = false);
+
     void setBaseColorId(const LLUUID& id, bool for_override = false);
     void setNormalId(const LLUUID& id, bool for_override = false);
-    void setMetallicRoughnessId(const LLUUID& id, bool for_override = false);
+    void setOcclusionRoughnessMetallicId(const LLUUID& id, bool for_override = false);
     void setEmissiveId(const LLUUID& id, bool for_override = false);
 
     void setBaseColorFactor(const LLColor4& baseColor, bool for_override = false);
@@ -182,6 +192,10 @@ class LLGLTFMaterial : public LLRefCount
 
     void applyOverride(const LLGLTFMaterial& override_mat);
 
+    // For base materials only (i.e. assets). Clears transforms to
+    // default since they're not supported in assets yet.
+    void sanitizeAssetMaterial();
+
     // For material overrides only. Clears most properties to
     // default/fallthrough, but preserves the transforms.
     bool setBaseMaterial();
@@ -189,12 +203,11 @@ class LLGLTFMaterial : public LLRefCount
     bool isClearedForBaseMaterial();
 
 private:
-
     template<typename T>
-    void setFromTexture(const tinygltf::Model& model, const T& texture_info, TextureInfo texture_info_id, LLUUID& texture_id_out);
+    void setFromTexture(const tinygltf::Model& model, const T& texture_info, TextureInfo texture_info_id);
 
     template<typename T>
-    void writeToTexture(tinygltf::Model& model, T& texture_info, TextureInfo texture_info_id, const LLUUID& texture_id) const;
+    void writeToTexture(tinygltf::Model& model, T& texture_info, TextureInfo texture_info_id, bool force_write = false) const;
 
     void setBaseMaterial(const LLGLTFMaterial& old_override_mat);
 };
diff --git a/indra/llprimitive/tests/llgltfmaterial_test.cpp b/indra/llprimitive/tests/llgltfmaterial_test.cpp
new file mode 100644
index 00000000000..4f58f4e567f
--- /dev/null
+++ b/indra/llprimitive/tests/llgltfmaterial_test.cpp
@@ -0,0 +1,255 @@
+/** 
+ * @file llgltfmaterial_test.cpp
+ *
+ * $LicenseInfo:firstyear=2023&license=viewerlgpl$                               
+ * Second Life Viewer Source Code
+ * Copyright (C) 2023, Linden Research, Inc.
+ * 
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ * 
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ * 
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
+ * $/LicenseInfo$                                                               
+ */
+
+#pragma once
+
+#include "linden_common.h"
+#include "lltut.h"
+
+#include "../llgltfmaterial.h"
+#include "lluuid.cpp"
+
+// Import & define single-header gltf import/export lib
+#define TINYGLTF_IMPLEMENTATION
+#define TINYGLTF_USE_CPP14  // default is C++ 11
+
+// tinygltf by default loads image files using STB
+#define STB_IMAGE_IMPLEMENTATION
+// to use our own image loading:
+// 1. replace this definition with TINYGLTF_NO_STB_IMAGE
+// 2. provide image loader callback with TinyGLTF::SetImageLoader(LoadimageDataFunction LoadImageData, void *user_data)
+
+// tinygltf saves image files using STB
+#define STB_IMAGE_WRITE_IMPLEMENTATION
+// similarly, can override with TINYGLTF_NO_STB_IMAGE_WRITE and TinyGLTF::SetImageWriter(fxn, data)
+
+// Disable reading external images to prevent warnings and speed up the tests.
+// We don't need this for the tests, but still need the filesystem
+// implementation to be defined in order for llprimitive to link correctly.
+#define TINYGLTF_NO_EXTERNAL_IMAGE 1
+
+#include "tinygltf/tiny_gltf.h"
+
+namespace tut
+{
+    struct llgltfmaterial
+    {
+    };
+    typedef test_group<llgltfmaterial> llgltfmaterial_t;
+    typedef llgltfmaterial_t::object llgltfmaterial_object_t;
+    tut::llgltfmaterial_t tut_llgltfmaterial("llgltfmaterial");
+
+    // A positive 32-bit float with a long string representation
+    constexpr F32 test_fraction = 1.09045365e-32;
+    // A larger positive 32-bit float for values that get zeroed if below a threshold
+    constexpr F32 test_fraction_big = 0.109045;
+
+    void apply_test_material_texture_ids(LLGLTFMaterial& material)
+    {
+        material.setBaseColorId(LLUUID::generateNewID());
+        material.setNormalId(LLUUID::generateNewID());
+        material.setOcclusionRoughnessMetallicId(LLUUID::generateNewID());
+        material.setEmissiveId(LLUUID::generateNewID());
+    }
+
+    void apply_test_material_texture_transforms(LLGLTFMaterial& material)
+    {
+        LLGLTFMaterial::TextureTransform test_transform;
+        test_transform.mOffset.mV[VX] = test_fraction;
+        test_transform.mOffset.mV[VY] = test_fraction;
+        test_transform.mScale.mV[VX] = test_fraction;
+        test_transform.mScale.mV[VY] = test_fraction;
+        test_transform.mRotation = test_fraction;
+        for (LLGLTFMaterial::TextureInfo i = LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR; i < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; i = LLGLTFMaterial::TextureInfo((U32)i + 1))
+        {
+            material.setTextureOffset(i, test_transform.mOffset);
+            material.setTextureScale(i, test_transform.mScale);
+            material.setTextureRotation(i, test_transform.mRotation);
+        }
+    }
+
+    void apply_test_material_factors(LLGLTFMaterial& material)
+    {
+        material.setBaseColorFactor(LLColor4(test_fraction_big, test_fraction_big, test_fraction_big, test_fraction_big));
+        material.setEmissiveColorFactor(LLColor3(test_fraction_big, test_fraction_big, test_fraction_big));
+        material.setMetallicFactor(test_fraction);
+        material.setRoughnessFactor(test_fraction);
+    }
+
+    LLGLTFMaterial create_test_material()
+    {
+        LLGLTFMaterial material;
+
+        apply_test_material_texture_ids(material);
+
+        apply_test_material_texture_transforms(material);
+
+        apply_test_material_factors(material);
+
+        material.setAlphaCutoff(test_fraction);
+        // Because this is the default value, it should append to the extras field to mark it as an override
+        material.setAlphaMode(LLGLTFMaterial::ALPHA_MODE_OPAQUE);
+        // Because this is the default value, it should append to the extras field to mark it as an override
+        material.setDoubleSided(false);
+
+        return material;
+    }
+
+    void ensure_gltf_material_serialize(const std::string& ensure_suffix, const LLGLTFMaterial& material_in)
+    {
+        const std::string json_in = material_in.asJSON();
+        LLGLTFMaterial material_out;
+        std::string warn_msg;
+        std::string error_msg;
+        bool serialize_success = material_out.fromJSON(json_in, warn_msg, error_msg);
+        ensure_equals("LLGLTFMaterial serialization has no warnings: " + ensure_suffix, "", warn_msg);
+        ensure_equals("LLGLTFMaterial serialization has no errors: " + ensure_suffix, "", error_msg);
+        ensure("LLGLTFMaterial serializes successfully: " + ensure_suffix, serialize_success);
+        ensure("LLGLTFMaterial is preserved when deserialized: " + ensure_suffix, material_in == material_out);
+        const std::string json_out = material_out.asJSON();
+        ensure_equals("LLGLTFMaterial is preserved when serialized: " + ensure_suffix, json_in, json_out);
+    }
+
+    void ensure_gltf_material_trimmed(const std::string& material_json, const std::string& must_not_contain)
+    {
+        ensure("LLGLTFMaterial serialization trims property '" + must_not_contain + "'", material_json.find(must_not_contain) == std::string::npos);
+    }
+
+    // Test that GLTF material fields have not changed since these tests were written
+    template<> template<>
+    void llgltfmaterial_object_t::test<1>()
+    {
+        if (sizeof(void*) > 4) // Don't bother running this test for 32-bit systems
+        {
+            // If any fields are added/changed, these tests should be updated (consider also updating ASSET_VERSION in LLGLTFMaterial)
+            ensure_equals("fields supported for GLTF (sizeof check)", sizeof(LLGLTFMaterial), 216);
+        }
+        ensure_equals("LLGLTFMaterial texture info count", (U32)LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT, 4);
+    }
+
+    // Test that occlusion and metallicRoughness are the same (They are different for asset validation. See lluploadmaterial.cpp)
+    template<> template<>
+    void llgltfmaterial_object_t::test<2>()
+    {
+        ensure_equals("LLGLTFMaterial occlusion does not differ from metallic roughness", LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS, LLGLTFMaterial::GLTF_TEXTURE_INFO_OCCLUSION);
+    }
+
+    // Ensure double sided and alpha mode overrides serialize as expected
+    template<> template<>
+    void llgltfmaterial_object_t::test<3>()
+    {
+        const bool doubleSideds[] { false, true };
+        const LLGLTFMaterial::AlphaMode alphaModes[] { LLGLTFMaterial::ALPHA_MODE_OPAQUE, LLGLTFMaterial::ALPHA_MODE_BLEND, LLGLTFMaterial::ALPHA_MODE_MASK };
+        const bool forOverrides[] { false, true };
+
+        for (bool doubleSided : doubleSideds)
+        {
+            for (bool forOverride : forOverrides)
+            {
+                LLGLTFMaterial material;
+                material.setDoubleSided(doubleSided, forOverride);
+                const bool overrideBit = (doubleSided == false) && forOverride;
+                ensure_equals("LLGLTFMaterial: double sided = " + std::to_string(doubleSided) + " override bit when forOverride = " + std::to_string(forOverride), material.mOverrideDoubleSided, overrideBit);
+                ensure_gltf_material_serialize("double sided = " + std::to_string(doubleSided), material);
+            }
+        }
+
+        for (LLGLTFMaterial::AlphaMode alphaMode : alphaModes)
+        {
+            for (bool forOverride : forOverrides)
+            {
+                LLGLTFMaterial material;
+                material.setAlphaMode(alphaMode, forOverride);
+                const bool overrideBit = (alphaMode == LLGLTFMaterial::ALPHA_MODE_OPAQUE) && forOverride;
+                ensure_equals("LLGLTFMaterial: alpha mode = " + std::to_string(alphaMode) + " override bit when forOverride = " + std::to_string(forOverride), material.mOverrideAlphaMode, overrideBit);
+                ensure_gltf_material_serialize("alpha mode = " + std::to_string(alphaMode), material);
+            }
+        }
+    }
+
+    // Test that a GLTF material's transform components serialize as expected
+    template<> template<>
+    void llgltfmaterial_object_t::test<4>()
+    {
+        LLGLTFMaterial material;
+        LLGLTFMaterial::TextureTransform& transform = material.mTextureTransform[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR];
+        transform.mOffset[VX] = 1.f;
+        transform.mOffset[VY] = 2.f;
+        transform.mScale[VX] = 0.05f;
+        transform.mScale[VY] = 100.f;
+        transform.mRotation = 1.571f;
+        ensure_gltf_material_serialize("material with transform", material);
+    }
+
+    // Test that a GLTF material avoids serializing a material unnecessarily
+    template<> template<>
+    void llgltfmaterial_object_t::test<5>()
+    {
+        {
+            const LLGLTFMaterial material;
+            const std::string material_json = material.asJSON();
+            ensure_gltf_material_trimmed(material_json, "pbrMetallicRoughness");
+            ensure_gltf_material_trimmed(material_json, "normalTexture");
+            ensure_gltf_material_trimmed(material_json, "emissiveTexture");
+            ensure_gltf_material_trimmed(material_json, "occlusionTexture");
+        }
+
+        {
+            LLGLTFMaterial metallic_factor_material;
+            metallic_factor_material.setMetallicFactor(0.5);
+            const std::string metallic_factor_material_json = metallic_factor_material.asJSON();
+            ensure_gltf_material_trimmed(metallic_factor_material_json, "baseColorTexture");
+            ensure_gltf_material_trimmed(metallic_factor_material_json, "metallicRoughnessTexture");
+        }
+    }
+
+    // Test that a GLTF material preserves values on serialization
+    template<> template<>
+    void llgltfmaterial_object_t::test<6>()
+    {
+        {
+            const LLGLTFMaterial full_material = create_test_material();
+            ensure_gltf_material_serialize("full material", full_material);
+        }
+
+        {
+            LLGLTFMaterial texture_ids_only_material;
+            apply_test_material_texture_ids(texture_ids_only_material);
+            ensure_gltf_material_serialize("material with texture IDs only", texture_ids_only_material);
+        }
+
+        {
+            LLGLTFMaterial texture_transforms_only_material;
+            apply_test_material_texture_ids(texture_transforms_only_material);
+            ensure_gltf_material_serialize("material with texture transforms only", texture_transforms_only_material);
+        }
+
+        {
+            LLGLTFMaterial factors_only_material;
+            apply_test_material_factors(factors_only_material);
+            ensure_gltf_material_serialize("material with scaling/tint factors only", factors_only_material);
+        }
+    }
+}
diff --git a/indra/newview/llgltfmateriallist.cpp b/indra/newview/llgltfmateriallist.cpp
index bc094ac8387..4051521ad4b 100644
--- a/indra/newview/llgltfmateriallist.cpp
+++ b/indra/newview/llgltfmateriallist.cpp
@@ -564,9 +564,9 @@ void LLGLTFMaterialList::onAssetLoadComplete(const LLUUID& id, LLAssetType::ETyp
 
                 if (LLSDSerialize::deserialize(asset, str, buffer.size()))
                 {
-                    if (asset.has("version") && asset["version"] == "1.0")
+                    if (asset.has("version") && LLGLTFMaterial::isAcceptedVersion(asset["version"].asString()))
                     {
-                        if (asset.has("type") && asset["type"].asString() == "GLTF 2.0")
+                        if (asset.has("type") && asset["type"].asString() == LLGLTFMaterial::ASSET_TYPE)
                         {
                             if (asset.has("data") && asset["data"].isString())
                             {
diff --git a/indra/newview/llmaterialeditor.cpp b/indra/newview/llmaterialeditor.cpp
index 5628327ebef..173f5620c67 100644
--- a/indra/newview/llmaterialeditor.cpp
+++ b/indra/newview/llmaterialeditor.cpp
@@ -277,10 +277,10 @@ bool LLSelectedTEGetMatData::apply(LLViewerObject* objectp, S32 te_index)
             llassert(mat.notNull()); // by this point shouldn't be null
             if (mat.notNull())
             {
-                tex_color_id = mat->mBaseColorId;
-                tex_metal_id = mat->mMetallicRoughnessId;
-                tex_emissive_id = mat->mEmissiveId;
-                tex_normal_id = mat->mNormalId;
+                tex_color_id = mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR];
+                tex_metal_id = mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS];
+                tex_emissive_id = mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE];
+                tex_normal_id = mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL];
             }
             if (mFirst)
             {
@@ -949,7 +949,7 @@ void LLMaterialEditor::onSelectCtrl(LLUICtrl* ctrl, const LLSD& data, S32 dirty_
                     }
                     case MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY:
                     {
-                        nodep->mSavedGLTFOverrideMaterials[te]->setMetallicRoughnessId(mCtrl->getValue().asUUID(), true);
+                        nodep->mSavedGLTFOverrideMaterials[te]->setOcclusionRoughnessMetallicId(mCtrl->getValue().asUUID(), true);
                         break;
                     }
                     case MATERIAL_EMISIVE_TEX_DIRTY:
@@ -991,30 +991,6 @@ void LLMaterialEditor::onSelectCtrl(LLUICtrl* ctrl, const LLSD& data, S32 dirty_
     LLSelectMgr::getInstance()->getSelection()->applyToNodes(&func);
 }
 
-static void write_color(const LLColor4& color, std::vector<double>& c)
-{
-    for (int i = 0; i < c.size(); ++i) // NOTE -- use c.size because some gltf colors are 3-component
-    {
-        c[i] = color.mV[i];
-    }
-}
-
-static U32 write_texture(const LLUUID& id, tinygltf::Model& model)
-{
-    tinygltf::Image image;
-    image.uri = id.asString();
-    model.images.push_back(image);
-    U32 image_idx = model.images.size() - 1;
-
-    tinygltf::Texture texture;
-    texture.source = image_idx;
-    model.textures.push_back(texture);
-    U32 texture_idx = model.textures.size() - 1;
-
-    return texture_idx;
-}
-
-
 void LLMaterialEditor::onClickSave()
 {
     if (!capabilitiesAvailable())
@@ -1034,109 +1010,14 @@ void LLMaterialEditor::onClickSave()
     saveIfNeeded();
 }
 
-
-std::string LLMaterialEditor::getGLTFJson(bool prettyprint)
-{
-    tinygltf::Model model;
-    getGLTFModel(model);
-
-    std::ostringstream str;
-
-    tinygltf::TinyGLTF gltf;
-    
-    gltf.WriteGltfSceneToStream(&model, str, prettyprint, false);
-
-    std::string dump = str.str();
-
-    return dump;
-}
-
-void LLMaterialEditor::getGLBData(std::vector<U8>& data)
-{
-    tinygltf::Model model;
-    getGLTFModel(model);
-
-    std::ostringstream str;
-
-    tinygltf::TinyGLTF gltf;
-
-    gltf.WriteGltfSceneToStream(&model, str, false, true);
-
-    std::string dump = str.str();
-
-    data.resize(dump.length());
-
-    memcpy(&data[0], dump.c_str(), dump.length());
-}
-
-void LLMaterialEditor::getGLTFModel(tinygltf::Model& model)
-{
-    model.materials.resize(1);
-    tinygltf::PbrMetallicRoughness& pbrMaterial = model.materials[0].pbrMetallicRoughness;
-
-    // write base color
-    LLColor4 base_color = getBaseColor();
-    base_color.mV[3] = getTransparency();
-    write_color(base_color, pbrMaterial.baseColorFactor);
-
-    model.materials[0].alphaCutoff = getAlphaCutoff();
-    model.materials[0].alphaMode = getAlphaMode();
-
-    LLUUID base_color_id = getBaseColorId();
-
-    if (base_color_id.notNull())
-    {
-        U32 texture_idx = write_texture(base_color_id, model);
-
-        pbrMaterial.baseColorTexture.index = texture_idx;
-    }
-
-    // write metallic/roughness
-    F32 metalness = getMetalnessFactor();
-    F32 roughness = getRoughnessFactor();
-
-    pbrMaterial.metallicFactor = metalness;
-    pbrMaterial.roughnessFactor = roughness;
-
-    LLUUID mr_id = getMetallicRoughnessId();
-    if (mr_id.notNull())
-    {
-        U32 texture_idx = write_texture(mr_id, model);
-        pbrMaterial.metallicRoughnessTexture.index = texture_idx;
-    }
-
-    //write emissive
-    LLColor4 emissive_color = getEmissiveColor();
-    model.materials[0].emissiveFactor.resize(3);
-    write_color(emissive_color, model.materials[0].emissiveFactor);
-
-    LLUUID emissive_id = getEmissiveId();
-    if (emissive_id.notNull())
-    {
-        U32 idx = write_texture(emissive_id, model);
-        model.materials[0].emissiveTexture.index = idx;
-    }
-
-    //write normal
-    LLUUID normal_id = getNormalId();
-    if (normal_id.notNull())
-    {
-        U32 idx = write_texture(normal_id, model);
-        model.materials[0].normalTexture.index = idx;
-    }
-
-    //write doublesided
-    model.materials[0].doubleSided = getDoubleSided();
-
-    model.asset.version = "2.0";
-}
-
 std::string LLMaterialEditor::getEncodedAsset()
 {
     LLSD asset;
-    asset["version"] = "1.0";
-    asset["type"] = "GLTF 2.0";
-    asset["data"] = getGLTFJson(false);
+    asset["version"] = LLGLTFMaterial::ASSET_VERSION;
+    asset["type"] = LLGLTFMaterial::ASSET_TYPE;
+    LLGLTFMaterial mat;
+    getGLTFMaterial(&mat);
+    asset["data"] = mat.asJSON();
 
     std::ostringstream str;
     LLSDSerialize::serialize(asset, str, LLSDSerialize::LLSD_BINARY);
@@ -1151,9 +1032,9 @@ bool LLMaterialEditor::decodeAsset(const std::vector<char>& buffer)
     std::istrstream str(&buffer[0], buffer.size());
     if (LLSDSerialize::deserialize(asset, str, buffer.size()))
     {
-        if (asset.has("version") && asset["version"] == "1.0")
+        if (asset.has("version") && LLGLTFMaterial::isAcceptedVersion(asset["version"].asString()))
         {
-            if (asset.has("type") && asset["type"] == "GLTF 2.0")
+            if (asset.has("type") && asset["type"] == LLGLTFMaterial::ASSET_TYPE)
             {
                 if (asset.has("data") && asset["data"].isString())
                 {
@@ -1169,6 +1050,12 @@ bool LLMaterialEditor::decodeAsset(const std::vector<char>& buffer)
                     if (loader.LoadASCIIFromString(&model_in, &error_msg, &warn_msg, data.c_str(), data.length(), ""))
                     {
                         // assets are only supposed to have one item
+                        // *NOTE: This duplicates some functionality from
+                        // LLGLTFMaterial::fromJSON, but currently does the job
+                        // better for the material editor use case.
+                        // However, LLGLTFMaterial::asJSON should always be
+                        // used when uploading materials, to ensure the
+                        // asset is valid.
                         return setFromGltfModel(model_in, 0, true);
                     }
                     else
@@ -1977,7 +1864,9 @@ void LLMaterialEditor::saveMaterialAs(const LLGLTFMaterial* render_material, con
         {
             // don't use override material here, it has 'hacked ids'
             // and values, use end result, apply it on top of local.
-            me->setBaseColor(render_material->mBaseColor);
+            const LLColor4& base_color = render_material->mBaseColor;
+            me->setBaseColor(LLColor3(base_color));
+            me->setTransparency(base_color[VW]);
             me->setMetalnessFactor(render_material->mMetallicFactor);
             me->setRoughnessFactor(render_material->mRoughnessFactor);
             me->setEmissiveColor(render_material->mEmissiveColor);
@@ -1988,24 +1877,24 @@ void LLMaterialEditor::saveMaterialAs(const LLGLTFMaterial* render_material, con
             // most things like colors we can apply without verifying
             // but texture ids are going to be different from both, base and override
             // so only apply override id if there is actually a difference
-            if (local_material->mBaseColorId != render_material->mBaseColorId)
+            if (local_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR] != render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR])
             {
-                me->setBaseColorId(render_material->mBaseColorId);
+                me->setBaseColorId(render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR]);
                 me->childSetValue("base_color_upload_fee", me->getString("no_upload_fee_string"));
             }
-            if (local_material->mNormalId != render_material->mNormalId)
+            if (local_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL] != render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL])
             {
-                me->setNormalId(render_material->mNormalId);
+                me->setNormalId(render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL]);
                 me->childSetValue("normal_upload_fee", me->getString("no_upload_fee_string"));
             }
-            if (local_material->mMetallicRoughnessId != render_material->mMetallicRoughnessId)
+            if (local_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS] != render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS])
             {
-                me->setMetallicRoughnessId(render_material->mMetallicRoughnessId);
+                me->setMetallicRoughnessId(render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS]);
                 me->childSetValue("metallic_upload_fee", me->getString("no_upload_fee_string"));
             }
-            if (local_material->mEmissiveId != render_material->mEmissiveId)
+            if (local_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE] != render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE])
             {
-                me->setEmissiveId(render_material->mEmissiveId);
+                me->setEmissiveId(render_material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE]);
                 me->childSetValue("emissive_upload_fee", me->getString("no_upload_fee_string"));
             }
 
@@ -2019,7 +1908,11 @@ void LLMaterialEditor::saveMaterialAs(const LLGLTFMaterial* render_material, con
     LLSD payload;
     if (render_material)
     {
-        payload["data"] = render_material->asJSON();
+        // Make a copy of the render material with unsupported transforms removed
+        LLGLTFMaterial asset_material = *render_material;
+        asset_material.sanitizeAssetMaterial();
+        // Serialize the sanitized render material
+        payload["data"] = asset_material.asJSON();
     }
     else
     {
@@ -2042,8 +1935,9 @@ void LLMaterialEditor::onSaveObjectsMaterialAsMsgCallback(const LLSD& notificati
     if (0 == option)
     {
         LLSD asset;
-        asset["version"] = "1.0";
-        asset["type"] = "GLTF 2.0";
+        asset["version"] = LLGLTFMaterial::ASSET_VERSION;
+        asset["type"] = LLGLTFMaterial::ASSET_TYPE;
+        // This is the string serialized from LLGLTFMaterial::asJSON
         asset["data"] = notification["payload"]["data"];
 
         std::ostringstream str;
@@ -2586,7 +2480,7 @@ class LLRenderMaterialOverrideFunctor : public LLSelectedNodeFunctor
             }
             else if ((reverted_flags & MATERIAL_BASE_COLOR_TEX_DIRTY) && revert_mat.notNull())
             {
-                material->setBaseColorId(revert_mat->mBaseColorId, false);
+                material->setBaseColorId(revert_mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR], false);
             }
 
             if (changed_flags & MATERIAL_NORMAL_TEX_DIRTY)
@@ -2595,16 +2489,16 @@ class LLRenderMaterialOverrideFunctor : public LLSelectedNodeFunctor
             }
             else if ((reverted_flags & MATERIAL_NORMAL_TEX_DIRTY) && revert_mat.notNull())
             {
-                material->setNormalId(revert_mat->mNormalId, false);
+                material->setNormalId(revert_mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL], false);
             }
 
             if (changed_flags & MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY)
             {
-                material->setMetallicRoughnessId(mEditor->getMetallicRoughnessId(), true);
+                material->setOcclusionRoughnessMetallicId(mEditor->getMetallicRoughnessId(), true);
             }
             else if ((reverted_flags & MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY) && revert_mat.notNull())
             {
-                material->setMetallicRoughnessId(revert_mat->mMetallicRoughnessId, false);
+                material->setOcclusionRoughnessMetallicId(revert_mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS], false);
             }
 
             if (changed_flags & MATERIAL_METALLIC_ROUGHTNESS_METALNESS_DIRTY)
@@ -2640,7 +2534,7 @@ class LLRenderMaterialOverrideFunctor : public LLSelectedNodeFunctor
             }
             else if ((reverted_flags & MATERIAL_EMISIVE_TEX_DIRTY) && revert_mat.notNull())
             {
-                material->setEmissiveId(revert_mat->mEmissiveId, false);
+                material->setEmissiveId(revert_mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE], false);
             }
 
             if (changed_flags & MATERIAL_DOUBLE_SIDED_DIRTY)
@@ -2753,20 +2647,23 @@ void LLMaterialEditor::applyToSelection()
     }
 }
 
+// Get a dump of the json representation of the current state of the editor UI
+// in GLTF format, excluding transforms as they are not supported in material
+// assets. (See also LLGLTFMaterial::sanitizeAssetMaterial())
 void LLMaterialEditor::getGLTFMaterial(LLGLTFMaterial* mat)
 {
     mat->mBaseColor = getBaseColor();
     mat->mBaseColor.mV[3] = getTransparency();
-    mat->mBaseColorId = getBaseColorId();
+    mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR] = getBaseColorId();
 
-    mat->mNormalId = getNormalId();
+    mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL] = getNormalId();
 
-    mat->mMetallicRoughnessId = getMetallicRoughnessId();
+    mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS] = getMetallicRoughnessId();
     mat->mMetallicFactor = getMetalnessFactor();
     mat->mRoughnessFactor = getRoughnessFactor();
 
     mat->mEmissiveColor = getEmissiveColor();
-    mat->mEmissiveId = getEmissiveId();
+    mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE] = getEmissiveId();
 
     mat->mDoubleSided = getDoubleSided();
     mat->setAlphaMode(getAlphaMode());
@@ -2776,15 +2673,15 @@ void LLMaterialEditor::getGLTFMaterial(LLGLTFMaterial* mat)
 void LLMaterialEditor::setFromGLTFMaterial(LLGLTFMaterial* mat)
 {
     setBaseColor(mat->mBaseColor);
-    setBaseColorId(mat->mBaseColorId);
-    setNormalId(mat->mNormalId);
+    setBaseColorId(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR]);
+    setNormalId(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL]);
 
-    setMetallicRoughnessId(mat->mMetallicRoughnessId);
+    setMetallicRoughnessId(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS]);
     setMetalnessFactor(mat->mMetallicFactor);
     setRoughnessFactor(mat->mRoughnessFactor);
 
     setEmissiveColor(mat->mEmissiveColor);
-    setEmissiveId(mat->mEmissiveId);
+    setEmissiveId(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE]);
 
     setDoubleSided(mat->mDoubleSided);
     setAlphaMode(mat->getAlphaMode());
diff --git a/indra/newview/llmaterialeditor.h b/indra/newview/llmaterialeditor.h
index 74c776031e5..0401190773f 100644
--- a/indra/newview/llmaterialeditor.h
+++ b/indra/newview/llmaterialeditor.h
@@ -84,8 +84,7 @@ class LLFloaterComboOptions : public LLFloater
 };
 
 class LLMaterialEditor : public LLPreview, public LLVOInventoryListener
-{
-public:
+{ public:
 	LLMaterialEditor(const LLSD& key);
 
     bool setFromGltfModel(const tinygltf::Model& model, S32 index, bool set_textures = false);
@@ -98,6 +97,7 @@ class LLMaterialEditor : public LLPreview, public LLVOInventoryListener
     // for live preview, apply current material to currently selected object
     void applyToSelection();
 
+    // get a dump of the json representation of the current state of the editor UI as a material object
     void getGLTFMaterial(LLGLTFMaterial* mat);
 
     void loadAsset() override;
@@ -131,11 +131,6 @@ class LLMaterialEditor : public LLPreview, public LLVOInventoryListener
 
     void onClickSave();
 
-    // get a dump of the json representation of the current state of the editor UI in GLTF format
-    std::string getGLTFJson(bool prettyprint = true);
-
-    void getGLBData(std::vector<U8>& data);
-
     void getGLTFModel(tinygltf::Model& model);
 
     std::string getEncodedAsset();
diff --git a/indra/newview/lltinygltfhelper.cpp b/indra/newview/lltinygltfhelper.cpp
index 611911014ac..1a8e868d119 100644
--- a/indra/newview/lltinygltfhelper.cpp
+++ b/indra/newview/lltinygltfhelper.cpp
@@ -309,48 +309,48 @@ bool LLTinyGLTFHelper::getMaterialFromFile(
     if (base_color_tex)
     {
         base_color_tex->addTextureStats(64.f * 64.f, TRUE);
-        material->mBaseColorId = base_color_tex->getID();
+        material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR] = base_color_tex->getID();
         material->mBaseColorTexture = base_color_tex;
     }
     else
     {
-        material->mBaseColorId = LLUUID::null;
+        material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR] = LLUUID::null;
         material->mBaseColorTexture = nullptr;
     }
 
     if (normal_tex)
     {
         normal_tex->addTextureStats(64.f * 64.f, TRUE);
-        material->mNormalId = normal_tex->getID();
+        material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL] = normal_tex->getID();
         material->mNormalTexture = normal_tex;
     }
     else
     {
-        material->mNormalId = LLUUID::null;
+        material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL] = LLUUID::null;
         material->mNormalTexture = nullptr;
     }
 
     if (mr_tex)
     {
         mr_tex->addTextureStats(64.f * 64.f, TRUE);
-        material->mMetallicRoughnessId = mr_tex->getID();
+        material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS] = mr_tex->getID();
         material->mMetallicRoughnessTexture = mr_tex;
     }
     else
     {
-        material->mMetallicRoughnessId = LLUUID::null;
+        material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS] = LLUUID::null;
         material->mMetallicRoughnessTexture = nullptr;
     }
 
     if (emissive_tex)
     {
         emissive_tex->addTextureStats(64.f * 64.f, TRUE);
-        material->mEmissiveId = emissive_tex->getID();
+        material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE] = emissive_tex->getID();
         material->mEmissiveTexture = emissive_tex;
     }
     else
     {
-        material->mEmissiveId = LLUUID::null;
+        material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE] = LLUUID::null;
         material->mEmissiveTexture = nullptr;
     }
 
diff --git a/indra/newview/llviewermenufile.cpp b/indra/newview/llviewermenufile.cpp
index 84b00105455..9743f00ab8e 100644
--- a/indra/newview/llviewermenufile.cpp
+++ b/indra/newview/llviewermenufile.cpp
@@ -543,19 +543,19 @@ bool get_bulk_upload_expected_cost(const std::vector<std::string>& filenames, S3
                     // Todo: make it account for possibility of same texture in different
                     // materials and even in scope of same material
                     S32 texture_count = 0;
-                    if (material->mBaseColorId.notNull())
+                    if (material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR].notNull())
                     {
                         texture_count++;
                     }
-                    if (material->mMetallicRoughnessId.notNull())
+                    if (material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS].notNull())
                     {
                         texture_count++;
                     }
-                    if (material->mNormalId.notNull())
+                    if (material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL].notNull())
                     {
                         texture_count++;
                     }
-                    if (material->mEmissiveId.notNull())
+                    if (material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE].notNull())
                     {
                         texture_count++;
                     }
diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp
index 5a365c79eab..88d5d50845f 100644
--- a/indra/newview/llviewerobject.cpp
+++ b/indra/newview/llviewerobject.cpp
@@ -4993,10 +4993,10 @@ void LLViewerObject::updateTEMaterialTextures(U8 te)
 
     if (mat != nullptr)
     {
-        mat->mBaseColorTexture = fetch_texture(mat->mBaseColorId);
-        mat->mNormalTexture = fetch_texture(mat->mNormalId);
-        mat->mMetallicRoughnessTexture = fetch_texture(mat->mMetallicRoughnessId);
-        mat->mEmissiveTexture= fetch_texture(mat->mEmissiveId);
+        mat->mBaseColorTexture = fetch_texture(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR]);
+        mat->mNormalTexture = fetch_texture(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL]);
+        mat->mMetallicRoughnessTexture = fetch_texture(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS]);
+        mat->mEmissiveTexture= fetch_texture(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE]);
     }
 }
 
-- 
GitLab