From 4522f33d2bad2cc0f67e10a0b0ad3cc7c1b43fbd Mon Sep 17 00:00:00 2001
From: Dave Parks <davep@lindenlab.com>
Date: Mon, 10 Jun 2024 16:57:31 -0500
Subject: #1677 Add GLTF extensions serialization and support for
 KHR_materials_unlit (#1686)

---
 indra/llmath/llvolumeoctree.cpp  |  6 +--
 indra/newview/gltf/asset.cpp     | 40 +++++++++++++++-
 indra/newview/gltf/asset.h       | 19 ++++++++
 indra/newview/gltf/buffer_util.h | 98 ++++++++++++++++++++++++++++++++++++++++
 indra/newview/gltf/primitive.cpp | 24 +++++++---
 5 files changed, 176 insertions(+), 11 deletions(-)

diff --git a/indra/llmath/llvolumeoctree.cpp b/indra/llmath/llvolumeoctree.cpp
index 7a4d313fb1..faa169e295 100644
--- a/indra/llmath/llvolumeoctree.cpp
+++ b/indra/llmath/llvolumeoctree.cpp
@@ -151,7 +151,7 @@ void LLOctreeTriangleRayIntersect::visit(const LLOctreeNode<LLVolumeTriangle, LL
                 U32 idx1 = tri->mIndex[1];
                 U32 idx2 = tri->mIndex[2];
 
-                if (mTexCoord != NULL)
+                if (mTexCoord != NULL && mFace->mTexCoords)
                 {
                     LLVector2* tc = (LLVector2*) mFace->mTexCoords;
                     *mTexCoord = ((1.f - a - b)  * tc[idx0] +
@@ -160,7 +160,7 @@ void LLOctreeTriangleRayIntersect::visit(const LLOctreeNode<LLVolumeTriangle, LL
 
                 }
 
-                if (mNormal != NULL)
+                if (mNormal != NULL && mFace->mNormals)
                 {
                     LLVector4a* norm = mFace->mNormals;
 
@@ -180,7 +180,7 @@ void LLOctreeTriangleRayIntersect::visit(const LLOctreeNode<LLVolumeTriangle, LL
                     *mNormal        = n1;
                 }
 
-                if (mTangent != NULL)
+                if (mTangent != NULL && mFace->mTangents)
                 {
                     LLVector4a* tangents = mFace->mTangents;
 
diff --git a/indra/newview/gltf/asset.cpp b/indra/newview/gltf/asset.cpp
index 7d379c2528..a4efb25860 100644
--- a/indra/newview/gltf/asset.cpp
+++ b/indra/newview/gltf/asset.cpp
@@ -39,10 +39,15 @@
 using namespace LL::GLTF;
 using namespace boost::json;
 
+
 namespace LL
 {
     namespace GLTF
     {
+        static std::unordered_set<std::string> ExtensionsSupported = {
+            "KHR_materials_unlit"
+        };
+
         Material::AlphaMode gltf_alpha_mode_to_enum(const std::string& alpha_mode)
         {
             if (alpha_mode == "OPAQUE")
@@ -382,6 +387,22 @@ void Asset::update()
 
 bool Asset::prep()
 {
+    // check required extensions and fail if not supported
+    bool unsupported = false;
+    for (auto& extension : mExtensionsRequired)
+    {
+        if (ExtensionsSupported.find(extension) == ExtensionsSupported.end())
+        {
+            LL_WARNS() << "Unsupported extension: " << extension << LL_ENDL;
+            unsupported = true;
+        }
+    }
+
+    if (unsupported)
+    {
+        return false;
+    }
+
     // do buffers first as other resources depend on them
     for (auto& buffer : mBuffers)
     {
@@ -600,6 +621,8 @@ const Asset& Asset::operator=(const Value& src)
         copy(obj, "accessors", mAccessors);
         copy(obj, "animations", mAnimations);
         copy(obj, "skins", mSkins);
+        copy(obj, "extensionsUsed", mExtensionsUsed);
+        copy(obj, "extensionsRequired", mExtensionsRequired);
     }
 
     return *this;
@@ -628,6 +651,8 @@ void Asset::serialize(object& dst) const
     write(mAccessors, "accessors", dst);
     write(mAnimations, "animations", dst);
     write(mSkins, "skins", dst);
+    write(mExtensionsUsed, "extensionsUsed", dst);
+    write(mExtensionsRequired, "extensionsRequired", dst);
 }
 
 bool Asset::save(const std::string& filename)
@@ -979,6 +1004,17 @@ bool Material::PbrMetallicRoughness::operator!=(const Material::PbrMetallicRough
     return !(*this == rhs);
 }
 
+const Material::Unlit& Material::Unlit::operator=(const Value& src)
+{
+    mPresent = true;
+    return *this;
+}
+
+void Material::Unlit::serialize(object& dst) const
+{
+    // no members and object has already been created, nothing to do
+}
+
 void Material::serialize(object& dst) const
 {
     write(mName, "name", dst);
@@ -990,6 +1026,7 @@ void Material::serialize(object& dst) const
     write(mAlphaMode, "alphaMode", dst, Material::AlphaMode::OPAQUE);
     write(mAlphaCutoff, "alphaCutoff", dst, 0.5f);
     write(mDoubleSided, "doubleSided", dst, false);
+    write_extensions(dst, &mUnlit, "KHR_materials_unlit");
 }
 
 const Material& Material::operator=(const Value& src)
@@ -1005,6 +1042,8 @@ const Material& Material::operator=(const Value& src)
         copy(src, "alphaMode", mAlphaMode);
         copy(src, "alphaCutoff", mAlphaCutoff);
         copy(src, "doubleSided", mDoubleSided);
+        copy_extensions(src,
+            "KHR_materials_unlit", &mUnlit );
     }
     return *this;
 }
@@ -1027,7 +1066,6 @@ const Mesh& Mesh::operator=(const Value& src)
     }
 
     return *this;
-
 }
 
 bool Mesh::prep(Asset& asset)
diff --git a/indra/newview/gltf/asset.h b/indra/newview/gltf/asset.h
index fd7ea17f05..8f28e5905f 100644
--- a/indra/newview/gltf/asset.h
+++ b/indra/newview/gltf/asset.h
@@ -49,10 +49,26 @@ namespace LL
     {
         class Asset;
 
+        class Extension
+        {
+        public:
+            // true if this extension is present in the gltf file
+            // otherwise false
+            bool mPresent = false;
+        };
+
+
         class Material
         {
         public:
 
+            class Unlit : public Extension // KHR_materials_unlit implementation
+            {
+            public:
+                const Unlit& operator=(const Value& src);
+                void serialize(boost::json::object& dst) const;
+            };
+
             enum class AlphaMode
             {
                 OPAQUE,
@@ -117,6 +133,7 @@ namespace LL
             AlphaMode mAlphaMode = AlphaMode::OPAQUE;
             F32 mAlphaCutoff = 0.5f;
             bool mDoubleSided = false;
+            Unlit mUnlit;
 
             const Material& operator=(const Value& src);
             void serialize(boost::json::object& dst) const;
@@ -300,6 +317,8 @@ namespace LL
             std::vector<Accessor> mAccessors;
             std::vector<Animation> mAnimations;
             std::vector<Skin> mSkins;
+            std::vector<std::string> mExtensionsUsed;
+            std::vector<std::string> mExtensionsRequired;
 
             std::string mVersion;
             std::string mGenerator;
diff --git a/indra/newview/gltf/buffer_util.h b/indra/newview/gltf/buffer_util.h
index bd43f60d19..c26752a6b6 100644
--- a/indra/newview/gltf/buffer_util.h
+++ b/indra/newview/gltf/buffer_util.h
@@ -519,6 +519,104 @@ namespace LL
             return true;
         }
 
+
+        // to/from extension
+
+        // for internal use only, use copy_extensions instead
+        template<typename T>
+        inline bool _copy_extension(const boost::json::object& extensions, std::string_view member, T* dst)
+        {
+            if (extensions.contains(member))
+            {
+                return copy(extensions.at(member), *dst);
+            }
+
+            return false;
+        }
+
+        // Copy all extensions from src.extensions to provided destinations
+        // Usage:
+        //  copy_extensions(src,
+        //                  "KHR_materials_unlit", &mUnlit,
+        //                  "KHR_materials_pbrSpecularGlossiness", &mPbrSpecularGlossiness);
+        // returns true if any of the extensions are copied
+        template<class... Types>
+        inline bool copy_extensions(const boost::json::value& src, Types... args)
+        {
+            // extract the extensions object (don't assume it exists and verify that it is an object)
+            if (src.is_object())
+            {
+                boost::json::object obj = src.get_object();
+                if (obj.contains("extensions"))
+                {
+                    const boost::json::value& extensions = obj.at("extensions");
+                    if (extensions.is_object())
+                    {
+                        const boost::json::object& ext_obj = extensions.as_object();
+                        bool success = false;
+                        // copy each extension, return true if any of them succeed, do not short circuit on success
+                        U32 count = sizeof...(args);
+                        for (U32 i = 0; i < count; i += 2)
+                        {
+                            if (_copy_extension(ext_obj, args...))
+                            {
+                                success = true;
+                            }
+                        }
+                        return success;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        // internal use aonly, use write_extensions instead
+        template<typename T>
+        inline bool _write_extension(boost::json::object& extensions, const T* src, string_view member)
+        {
+            if (src->mPresent)
+            {
+                Value v;
+                if (write(*src, v))
+                {
+                    extensions[member] = v;
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        // Write all extensions to dst.extensions
+        // Usage:
+        //  write_extensions(dst,
+        //                   "KHR_materials_unlit", mUnlit,
+        //                   "KHR_materials_pbrSpecularGlossiness", mPbrSpecularGlossiness);
+        // returns true if any of the extensions are written
+        template<class... Types>
+        inline bool write_extensions(boost::json::object& dst, Types... args)
+        {
+            bool success = false;
+
+            boost::json::object extensions;
+            U32 count = sizeof...(args) - 1;
+
+            for (U32 i = 0; i < count; i += 2)
+            {
+                if (_write_extension(extensions, args...))
+                {
+                    success = true;
+                }
+            }
+
+            if (success)
+            {
+                dst["extensions"] = extensions;
+            }
+
+            return success;
+        }
+
         // conditionally write a member to an object if the member
         // is not the default value
         template<typename T>
diff --git a/indra/newview/gltf/primitive.cpp b/indra/newview/gltf/primitive.cpp
index 8d9880951b..bc333aff69 100644
--- a/indra/newview/gltf/primitive.cpp
+++ b/indra/newview/gltf/primitive.cpp
@@ -313,6 +313,10 @@ bool Primitive::prep(Asset& asset)
     // TODO: support colorless vertex buffers
     mask |= LLVertexBuffer::MAP_COLOR;
 
+    mShaderVariant = 0;
+
+    bool unlit = false;
+
     // bake material basecolor into color array
     if (mMaterial != INVALID_INDEX)
     {
@@ -322,11 +326,15 @@ bool Primitive::prep(Asset& asset)
         {
             dst = LLColor4U(baseColor * LLColor4(dst));
         }
-    }
 
-    mShaderVariant = 0;
+        if (material.mUnlit.mPresent)
+        { // material uses KHR_materials_unlit
+            mShaderVariant |= LLGLSLShader::GLTFVariant::UNLIT;
+            unlit = true;
+        }
+    }
 
-    if (mNormals.empty())
+    if (mNormals.empty() && !unlit)
     {
         mTangents.clear();
 
@@ -334,6 +342,7 @@ bool Primitive::prep(Asset& asset)
         { //no normals and no surfaces, this primitive is unlit
             mTangents.clear();
             mShaderVariant |= LLGLSLShader::GLTFVariant::UNLIT;
+            unlit = true;
         }
         else
         {
@@ -350,8 +359,6 @@ bool Primitive::prep(Asset& asset)
         }
     }
 
-    bool unlit = (mShaderVariant & LLGLSLShader::GLTFVariant::UNLIT) != 0;
-
     if (mTangents.empty() && !unlit)
     { // NOTE: must be done last because tangent generation rewrites the other arrays
         // adapted from usage of Mikktspace in llvolume.cpp
@@ -388,10 +395,13 @@ bool Primitive::prep(Asset& asset)
         }
     }
 
-
-    if (!unlit)
+    if (!mNormals.empty())
     {
         mask |= LLVertexBuffer::MAP_NORMAL;
+    }
+
+    if (!mTangents.empty())
+    {
         mask |= LLVertexBuffer::MAP_TANGENT;
     }
 
-- 
cgit v1.2.3