소스 검색

Adds the ability to generate UVs automatically during mesh import (#15530) (#15675)

* Adds the ability to generate UVs automatically during mesh import

This adds a new modifier to the available list of modifiers you can
add to a mesh during its import in its import settings.

The new modifier lets you select how to generate UVs for a mesh.
Currently, only 1 generator is supported (Spherical Positional
projection) but all the hard work here is done to support additional
unwrappers in the future by following the pattern.

This ability is crucial for workflows to do with importing robotic
meshes as they often lack UVs, and the path to otherwise get something
reasonable to render is painful

Signed-off-by: Nicholas Lawson <[email protected]>
Nicholas Lawson 2 년 전
부모
커밋
a65fc4e4a3
18개의 변경된 파일805개의 추가작업 그리고 38개의 파일을 삭제
  1. 5 0
      Code/Tools/SceneAPI/SceneData/GraphData/MeshVertexUVData.cpp
  2. 2 0
      Code/Tools/SceneAPI/SceneData/GraphData/MeshVertexUVData.h
  3. 5 0
      Code/Tools/SceneAPI/SceneData/ManifestMetaInfoHandler.cpp
  4. 2 0
      Code/Tools/SceneAPI/SceneData/ReflectionRegistrar.cpp
  5. 129 0
      Code/Tools/SceneAPI/SceneData/Rules/UVsRule.cpp
  6. 73 0
      Code/Tools/SceneAPI/SceneData/Rules/UVsRule.h
  7. 2 0
      Code/Tools/SceneAPI/SceneData/SceneData_files.cmake
  8. 6 0
      Gems/SceneProcessing/Code/Source/Generation/Components/TangentGenerator/TangentPreExportComponent.cpp
  9. 5 0
      Gems/SceneProcessing/Code/Source/Generation/Components/TangentGenerator/TangentPreExportComponent.h
  10. 304 0
      Gems/SceneProcessing/Code/Source/Generation/Components/UVsGenerator/UVsGenerateComponent.cpp
  11. 54 0
      Gems/SceneProcessing/Code/Source/Generation/Components/UVsGenerator/UVsGenerateComponent.h
  12. 54 0
      Gems/SceneProcessing/Code/Source/Generation/Components/UVsGenerator/UVsGenerators/SphereMappingUVsGenerator.cpp
  13. 22 0
      Gems/SceneProcessing/Code/Source/Generation/Components/UVsGenerator/UVsGenerators/SphereMappingUVsGenerator.h
  14. 63 0
      Gems/SceneProcessing/Code/Source/Generation/Components/UVsGenerator/UVsPreExportComponent.cpp
  15. 23 0
      Gems/SceneProcessing/Code/Source/Generation/Components/UVsGenerator/UVsPreExportComponent.h
  16. 4 0
      Gems/SceneProcessing/Code/Source/SceneProcessingModule.cpp
  17. 6 0
      Gems/SceneProcessing/Code/sceneprocessing_editor_static_files.cmake
  18. 46 38
      Registry/sceneassetimporter.setreg

+ 5 - 0
Code/Tools/SceneAPI/SceneData/GraphData/MeshVertexUVData.cpp

@@ -75,6 +75,11 @@ namespace AZ
                 m_uvs.reserve(size);
             }
 
+            void MeshVertexUVData::Clear()
+            {
+                m_uvs.clear();
+            }
+
             void MeshVertexUVData::AppendUV(const AZ::Vector2& uv)
             {
                 m_uvs.push_back(uv);

+ 2 - 0
Code/Tools/SceneAPI/SceneData/GraphData/MeshVertexUVData.h

@@ -43,6 +43,8 @@ namespace AZ
                 SCENE_DATA_API void ReserveContainerSpace(size_t size);
                 SCENE_DATA_API void AppendUV(const AZ::Vector2& uv);
 
+                SCENE_DATA_API void Clear();
+
                 SCENE_DATA_API void GetDebugOutput(AZ::SceneAPI::Utilities::DebugOutput& output) const override;
             protected:
                 AZStd::vector<AZ::Vector2> m_uvs;

+ 5 - 0
Code/Tools/SceneAPI/SceneData/ManifestMetaInfoHandler.cpp

@@ -28,6 +28,7 @@
 #include <SceneAPI/SceneData/Rules/SkinMeshAdvancedRule.h>
 #include <SceneAPI/SceneData/Rules/SkinRule.h>
 #include <SceneAPI/SceneData/Rules/CoordinateSystemRule.h>
+#include <SceneAPI/SceneData/Rules/UVsRule.h>
 
 namespace AZ
 {
@@ -85,6 +86,10 @@ namespace AZ
                     {
                         modifiers.push_back(azrtti_typeid<CoordinateSystemRule>());
                     }
+                    if (existingRules.find(SceneData::UVsRule::TYPEINFO_Uuid()) == existingRules.end())
+                    {
+                        modifiers.push_back(SceneData::UVsRule::TYPEINFO_Uuid());
+                    }
                     if (existingRules.find(SceneData::TangentsRule::TYPEINFO_Uuid()) == existingRules.end())
                     {
                         modifiers.push_back(SceneData::TangentsRule::TYPEINFO_Uuid());

+ 2 - 0
Code/Tools/SceneAPI/SceneData/ReflectionRegistrar.cpp

@@ -21,6 +21,7 @@
 #include <SceneAPI/SceneData/Rules/ScriptProcessorRule.h>
 #include <SceneAPI/SceneData/Rules/SkeletonProxyRule.h>
 #include <SceneAPI/SceneData/Rules/TangentsRule.h>
+#include <SceneAPI/SceneData/Rules/UVsRule.h>
 #include <SceneAPI/SceneData/Rules/CoordinateSystemRule.h>
 
 #include <SceneAPI/SceneData/ManifestBase/SceneNodeSelectionList.h>
@@ -74,6 +75,7 @@ namespace AZ
             SceneData::SkeletonProxyRule::Reflect(context);
             SceneData::SkinMeshAdvancedRule::Reflect(context);
             SceneData::TangentsRule::Reflect(context);
+            SceneData::UVsRule::Reflect(context);
             SceneData::CoordinateSystemRule::Reflect(context);
 
             // Utility

+ 129 - 0
Code/Tools/SceneAPI/SceneData/Rules/UVsRule.cpp

@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <SceneAPI/SceneData/Rules/UVsRule.h>
+#include <AzCore/Serialization/SerializeContext.h>
+#include <AzCore/Serialization/EditContext.h>
+#include <SceneAPI/SceneCore/Utilities/Reporting.h>
+#include <AzCore/Settings/SettingsRegistry.h>
+
+namespace AZ
+{
+    namespace SceneAPI
+    {
+        namespace SceneData
+        {
+            static constexpr AZStd::string_view DefaultUVsGenerationMethodKeyIfNoRulePresent{
+                "/O3DE/SceneAPI/UVsGenerateComponent/DefaultGenerationMethodIfNoRulePresent"
+            };
+
+            static constexpr AZStd::string_view DefaultUVsGenerationMethodKeyWhenAddingNewRules{
+                "/O3DE/SceneAPI/UVsGenerateComponent/DefaultGenerationMethodWhenRuleIsPresent"
+            };
+
+
+            UVsRule::UVsRule()
+                : DataTypes::IRule()
+            {
+                m_generationMethod = GetDefaultGenerationMethodWhenAddingNewRule();
+            }
+
+            AZ::SceneAPI::DataTypes::UVsGenerationMethod GetGenerationMethodFromRegistry(
+                AZStd::string_view regKey, AZ::SceneAPI::DataTypes::UVsGenerationMethod defaultValue)
+            {
+                if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
+                {
+                    AZStd::string stringFromRegistry;
+                    if (settingsRegistry->Get(stringFromRegistry, regKey))
+                    {
+                        const bool isCaseSensitive = false;
+                        if (AZ::StringFunc::Equal(stringFromRegistry, "LeaveSceneDataAsIs", isCaseSensitive))
+                        {
+                            return AZ::SceneAPI::DataTypes::UVsGenerationMethod::LeaveSceneDataAsIs;
+                        }
+                        else if (AZ::StringFunc::Equal(stringFromRegistry, "SphericalProjection", isCaseSensitive))
+                        {
+                            return AZ::SceneAPI::DataTypes::UVsGenerationMethod::SphericalProjection;
+                        }
+                        else
+                        {
+                            AZ_Warning(
+                                AZ::SceneAPI::Utilities::WarningWindow,
+                                false,
+                                "'%s' is not a valid default UV generation method. Check the value of " AZ_STRING_FORMAT " in your "
+                                "settings registry, and change "
+                                "it to 'LeaveSceneDataAsIs' or 'SphericalProjection'",
+                                stringFromRegistry.c_str(),
+                                AZ_STRING_ARG(regKey));
+                        }
+                    }
+                }
+                return defaultValue;
+            }
+
+            //! return the default method for when a new rule is explicitly created by script or user
+            AZ::SceneAPI::DataTypes::UVsGenerationMethod UVsRule::GetDefaultGenerationMethodWhenAddingNewRule()
+            {
+                // When someone goes to the effort of actually adding a new rule, make the default actually do something
+                return GetGenerationMethodFromRegistry(DefaultUVsGenerationMethodKeyWhenAddingNewRules,
+                    AZ::SceneAPI::DataTypes::UVsGenerationMethod::SphericalProjection);
+            }
+
+            AZ::SceneAPI::DataTypes::UVsGenerationMethod UVsRule::GetDefaultGenerationMethodWithNoRule()
+            {
+                // when there is no rule on the mesh, do nothing by default
+                return GetGenerationMethodFromRegistry(DefaultUVsGenerationMethodKeyIfNoRulePresent,
+                    AZ::SceneAPI::DataTypes::UVsGenerationMethod::LeaveSceneDataAsIs);
+            }
+
+            AZ::SceneAPI::DataTypes::UVsGenerationMethod UVsRule::GetGenerationMethod() const
+            {
+                return m_generationMethod;
+            }
+
+            bool UVsRule::GetReplaceExisting() const
+            {
+                return m_replaceExisting;
+            }
+
+            void UVsRule::Reflect(AZ::ReflectContext* context)
+            {
+                AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
+                if (!serializeContext)
+                {
+                    return;
+                }
+
+                serializeContext->Class<UVsRule, DataTypes::IRule>()
+                    ->Version(1)
+                    ->Field("generationMethod", &UVsRule::m_generationMethod)
+                    ->Field("replaceExisting", &UVsRule::m_replaceExisting);
+
+                AZ::EditContext* editContext = serializeContext->GetEditContext();
+                if (editContext)
+                {
+                    editContext->Class<UVsRule>("UVs", "Specify how UVs are imported or generated.")
+                        ->ClassElement(Edit::ClassElements::EditorData, "")
+                        ->Attribute("AutoExpand", true)
+                        ->Attribute(AZ::Edit::Attributes::NameLabelOverride, "")
+                        ->DataElement(
+                            AZ::Edit::UIHandlers::ComboBox,
+                            &AZ::SceneAPI::SceneData::UVsRule::m_generationMethod,
+                            "Generation Method",
+                            "Specify the UVs generation method when UVs are generated.")
+                        ->EnumAttribute(AZ::SceneAPI::DataTypes::UVsGenerationMethod::LeaveSceneDataAsIs, "Do not generate UVs")
+                        ->EnumAttribute(AZ::SceneAPI::DataTypes::UVsGenerationMethod::SphericalProjection, "Spherical Projection")
+                        ->DataElement(0, &AZ::SceneAPI::SceneData::UVsRule::m_replaceExisting,
+                            "Replace existing UVs",
+                            "If true, will replace UVs in the source scene even if present in the incoming data.")
+                    ;
+                }
+            }
+        } // SceneData
+    } // SceneAPI
+} // AZ

+ 73 - 0
Code/Tools/SceneAPI/SceneData/Rules/UVsRule.h

@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/Memory/SystemAllocator.h>
+#include <SceneAPI/SceneCore/DataTypes/Rules/IRule.h>
+#include <SceneAPI/SceneData/SceneDataConfiguration.h>
+
+namespace AZ
+{
+    class ReflectContext;
+
+    namespace SceneAPI
+    {
+        namespace Containers
+        {
+            class Scene;
+        }
+
+        namespace DataTypes
+        {
+            class IMeshVertexUVData;
+            enum class UVsGenerationMethod
+            {
+                LeaveSceneDataAsIs = 0, //! don't do anything to the scene
+                SphericalProjection = 1 //! generate UVs using simple spherical positional projection
+            };
+        }
+
+        namespace SceneData
+        {
+            //! The UVsRule class contains the settings for one particular instance of the "Generate UVs" modifier
+            //! on one particular mesh group in the scene.
+            class SCENE_DATA_CLASS UVsRule
+                : public DataTypes::IRule
+            {
+            public:
+                AZ_RTTI(UVsRule, "{79FB186C-E9B2-4569-9172-84B85DF81DB9}", DataTypes::IRule);
+                AZ_CLASS_ALLOCATOR(UVsRule, AZ::SystemAllocator)
+
+                SCENE_DATA_API UVsRule();
+                SCENE_DATA_API ~UVsRule() override = default;
+
+                SCENE_DATA_API AZ::SceneAPI::DataTypes::UVsGenerationMethod GetGenerationMethod() const;
+                SCENE_DATA_API bool GetReplaceExisting() const;
+
+                static void Reflect(ReflectContext* context);
+
+                // it can be useful to have a different default for when there is no rule ("do nothing" for example)
+                // versus if the user actually clicks a button or something to cause a rule to exist now, ie, actually do something
+                // useful.
+
+                //! Return the default method for UV Generation when a Generate UVs rule is attached as a modifier to a mesh group.
+                SCENE_DATA_API static AZ::SceneAPI::DataTypes::UVsGenerationMethod GetDefaultGenerationMethodWhenAddingNewRule();
+
+                //! Return the default method for when there is no Generate UVs rule attached to the mesh group.
+                //! this should probably be left as "do nothing" unless you want to auto-generate UVs for everything without UVs
+                SCENE_DATA_API static AZ::SceneAPI::DataTypes::UVsGenerationMethod GetDefaultGenerationMethodWithNoRule();
+
+            protected:
+                AZ::SceneAPI::DataTypes::UVsGenerationMethod m_generationMethod = AZ::SceneAPI::DataTypes::UVsGenerationMethod::LeaveSceneDataAsIs;
+                bool m_replaceExisting = false;
+
+            };
+        } // SceneData
+    } // SceneAPI
+} // AZ

+ 2 - 0
Code/Tools/SceneAPI/SceneData/SceneData_files.cmake

@@ -67,6 +67,8 @@ set(FILES
     Rules/SkinRule.cpp
     Rules/TangentsRule.h
     Rules/TangentsRule.cpp
+    Rules/UVsRule.h
+    Rules/UVsRule.cpp
     Rules/UnmodifiableRule.h
     Rules/UnmodifiableRule.cpp
     GraphData/CustomPropertyData.h

+ 6 - 0
Gems/SceneProcessing/Code/Source/Generation/Components/TangentGenerator/TangentPreExportComponent.cpp

@@ -21,6 +21,12 @@ namespace AZ::SceneGenerationComponents
         BindToCall(&TangentPreExportComponent::Register);
     }
 
+    uint8_t TangentPreExportComponent::GetPriority() const
+    {
+        return AZ::SceneAPI::Events::CallProcessor::ProcessingPriority::LateProcessing;
+    }
+
+
     void TangentPreExportComponent::Reflect(AZ::ReflectContext* context)
     {
         AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);

+ 5 - 0
Gems/SceneProcessing/Code/Source/Generation/Components/TangentGenerator/TangentPreExportComponent.h

@@ -28,6 +28,11 @@ namespace AZ::SceneGenerationComponents
 
         static void Reflect(AZ::ReflectContext* context);
 
+        // bumps Tangent export to later on in the generation phase, so that it can generate tangents after other rules have
+        // generated things like normals and UVs.
+
+        uint8_t GetPriority() const override;
+
         AZ::SceneAPI::Events::ProcessingResult Register(AZ::SceneAPI::Events::GenerateAdditionEventContext& context);
     };
 } // namespace AZ::SceneGenerationComponents

+ 304 - 0
Gems/SceneProcessing/Code/Source/Generation/Components/UVsGenerator/UVsGenerateComponent.cpp

@@ -0,0 +1,304 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Generation/Components/UVsGenerator/UVsGenerateComponent.h>
+#include <Generation/Components/UVsGenerator/UVsGenerators/SphereMappingUVsGenerator.h>
+
+#include <AzCore/std/smart_ptr/make_shared.h>
+
+#include <SceneAPI/SceneCore/Components/GenerationComponent.h>
+#include <SceneAPI/SceneCore/Containers/Views/PairIterator.h>
+#include <SceneAPI/SceneCore/Containers/Views/SceneGraphDownwardsIterator.h>
+#include <SceneAPI/SceneCore/Containers/Views/SceneGraphChildIterator.h>
+#include <SceneAPI/SceneCore/DataTypes/DataTypeUtilities.h>
+#include <SceneAPI/SceneCore/DataTypes/Groups/IGroup.h>
+#include <SceneAPI/SceneCore/DataTypes/GraphData/IMeshData.h>
+#include <SceneAPI/SceneCore/DataTypes/GraphData/IMeshVertexUVData.h>
+#include <SceneAPI/SceneData/GraphData/MeshVertexUVData.h>
+#include <SceneAPI/SceneData/Rules/UVsRule.h>
+#include <SceneAPI/SceneCore/Utilities/Reporting.h>
+
+namespace AZ::SceneGenerationComponents
+{
+    //! Check whether UVs are to be generated, and if so, generate them.
+    class UVsGenerateComponent : public AZ::SceneAPI::SceneCore::GenerationComponent
+    {
+    public:
+        AZ_COMPONENT(UVsGenerateComponent, s_UVsGenerateComponentTypeId, SceneAPI::SceneCore::GenerationComponent);
+
+        UVsGenerateComponent();
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        // Invoked by the CallProcessorBinder flow.  This is essentially the entry point for this operation.
+        AZ::SceneAPI::Events::ProcessingResult GenerateUVsData(UVsGenerateContext& context);
+
+    private:
+        bool GenerateUVsForMesh(
+            AZ::SceneAPI::Containers::Scene& scene,
+            const AZ::SceneAPI::Containers::SceneGraph::NodeIndex& nodeIndex,
+            AZ::SceneAPI::DataTypes::IMeshData* meshData,
+            const AZ::SceneAPI::DataTypes::UVsGenerationMethod generationMethod,
+            const bool replaceExisting);
+
+        //! How many UV Sets already exist on the mesh?
+        size_t CalcUvSetCount(
+            AZ::SceneAPI::Containers::SceneGraph& graph, const AZ::SceneAPI::Containers::SceneGraph::NodeIndex& nodeIndex) const;
+
+        //! find the Nth UV Set on the mesh and return it.
+        AZ::SceneAPI::DataTypes::IMeshVertexUVData* FindUvData(
+            AZ::SceneAPI::Containers::SceneGraph& graph,
+            const AZ::SceneAPI::Containers::SceneGraph::NodeIndex& nodeIndex,
+            AZ::u64 uvSet) const;
+
+        //! Return the UV Rule (the modifier on the mesh group) or nullptr if no such modifier is applied.
+        const AZ::SceneAPI::SceneData::UVsRule* GetUVsRule(const AZ::SceneAPI::Containers::Scene& scene) const;
+
+        //! Create a new UV set and hook it into the scene graph
+        bool CreateUVsLayer(
+            AZ::SceneAPI::Containers::SceneManifest& manifest,
+            const AZ::SceneAPI::Containers::SceneGraph::NodeIndex& nodeIndex,
+            AZ::SceneAPI::Containers::SceneGraph& graph,
+            SceneData::GraphData::MeshVertexUVData** outUVsData);
+    };
+
+    AZ::ComponentDescriptor* CreateUVsGenerateComponentDescriptor()
+    {
+        return UVsGenerateComponent::CreateDescriptor();
+    }
+
+    UVsGenerateComponent::UVsGenerateComponent()
+    {
+        BindToCall(&UVsGenerateComponent::GenerateUVsData);
+    }
+
+    void UVsGenerateComponent::Reflect(AZ::ReflectContext* context)
+    {
+        AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
+        if (serializeContext)
+        {
+            serializeContext->Class<UVsGenerateComponent, AZ::SceneAPI::SceneCore::GenerationComponent>()->Version(1);
+        }
+    }
+
+    const AZ::SceneAPI::SceneData::UVsRule* UVsGenerateComponent::GetUVsRule(const AZ::SceneAPI::Containers::Scene& scene) const
+    {
+        for (const auto& object : scene.GetManifest().GetValueStorage())
+        {
+            if (object->RTTI_IsTypeOf(AZ::SceneAPI::DataTypes::IGroup::TYPEINFO_Uuid()))
+            {
+                const AZ::SceneAPI::DataTypes::IGroup* group = azrtti_cast<const AZ::SceneAPI::DataTypes::IGroup*>(object.get());
+                const AZ::SceneAPI::SceneData::UVsRule* rule = group->GetRuleContainerConst().FindFirstByType<AZ::SceneAPI::SceneData::UVsRule>().get();
+                if (rule)
+                {
+                    return rule;
+                }
+            }
+        }
+
+        return nullptr;
+    }
+
+    AZ::SceneAPI::Events::ProcessingResult UVsGenerateComponent::GenerateUVsData(UVsGenerateContext& context)
+    {
+        // this component runs regardless of what modifiers are present on the mesh.
+        // Set some defaults to use if no settings are present at all:
+        AZ::SceneAPI::DataTypes::UVsGenerationMethod defaultGenerationMethod =
+            AZ::SceneAPI::SceneData::UVsRule::GetDefaultGenerationMethodWithNoRule();
+        bool defaultReplaceExisting = false;
+
+        const AZ::SceneAPI::SceneData::UVsRule* uvsRule = GetUVsRule(context.GetScene());
+        const AZ::SceneAPI::DataTypes::UVsGenerationMethod generationMethod = uvsRule ? uvsRule->GetGenerationMethod() : defaultGenerationMethod;
+        const bool replaceExisting = uvsRule ? uvsRule->GetReplaceExisting() : defaultReplaceExisting;
+
+        if (generationMethod == SceneAPI::DataTypes::UVsGenerationMethod::LeaveSceneDataAsIs)
+        {
+            // no point in going any further if the rule basically says to leave scene data as is
+            return AZ::SceneAPI::Events::ProcessingResult::Success;
+        }
+
+        // Iterate over all graph content and filter out all meshes.
+        AZ::SceneAPI::Containers::SceneGraph& graph = context.GetScene().GetGraph();
+        AZ::SceneAPI::Containers::SceneGraph::ContentStorageData graphContent = graph.GetContentStorage();
+
+        // Build a list of mesh data nodes.
+        AZStd::vector<AZStd::pair<AZ::SceneAPI::DataTypes::IMeshData*, AZ::SceneAPI::Containers::SceneGraph::NodeIndex> > meshes;
+        for (auto item = graphContent.begin(); item != graphContent.end(); ++item)
+        {
+            // Skip anything that isn't a mesh.
+            if (!(*item) || !(*item)->RTTI_IsTypeOf(AZ::SceneAPI::DataTypes::IMeshData::TYPEINFO_Uuid()))
+            {
+                continue;
+            }
+
+            // Get the mesh data and node index and store them in the vector as a pair, so we can iterate over them later.
+            auto* mesh = static_cast<AZ::SceneAPI::DataTypes::IMeshData*>(item->get());
+            AZ::SceneAPI::Containers::SceneGraph::NodeIndex nodeIndex = graph.ConvertToNodeIndex(item);
+            meshes.emplace_back(mesh, nodeIndex);
+        }
+
+        // Iterate over them. We had to build the array before as this method can insert new nodes, so using the iterator directly would fail.
+        for (auto& [mesh, nodeIndex] : meshes)
+        {
+            // Generate UVs for the mesh
+            if (!GenerateUVsForMesh(context.GetScene(), nodeIndex, mesh, generationMethod, replaceExisting))
+            {
+                return AZ::SceneAPI::Events::ProcessingResult::Failure;
+            }
+        }
+
+        return AZ::SceneAPI::Events::ProcessingResult::Success;
+    }
+
+    bool UVsGenerateComponent::GenerateUVsForMesh(
+        AZ::SceneAPI::Containers::Scene& scene,
+        const AZ::SceneAPI::Containers::SceneGraph::NodeIndex& nodeIndex,
+        AZ::SceneAPI::DataTypes::IMeshData* meshData,
+        const AZ::SceneAPI::DataTypes::UVsGenerationMethod generationMethod,
+        const bool replaceExisting)
+    {
+        AZ::SceneAPI::Containers::SceneGraph& graph = scene.GetGraph();
+        size_t uvSetCount = CalcUvSetCount(graph, nodeIndex);
+
+        // there might already be existing data there - see if there is.
+        AZ::SceneData::GraphData::MeshVertexUVData* dataToFill = nullptr;
+        if (uvSetCount > 0)
+        {
+            // This modifier always works on UV Set #0.
+            dataToFill = static_cast<AZ::SceneData::GraphData::MeshVertexUVData*>(FindUvData(graph, nodeIndex, 0));
+        }
+        AZStd::string currentNodeName = graph.GetNodeName(nodeIndex).GetPath();
+        if ((dataToFill) && (!replaceExisting))
+        {
+            // if there's already data, and we are not set to replace existing, do not generate data
+            AZ_Info(
+                AZ::SceneAPI::Utilities::LogWindow,
+                "Asked to generate UVs for mesh " AZ_STRING_FORMAT " but it already has UVs and 'replace existing' is not set.  Not replacing existing data.\n",
+                AZ_STRING_ARG(currentNodeName));
+            return true; // this is not an error!
+        }
+
+        if (!dataToFill)
+        {
+            if (!CreateUVsLayer(scene.GetManifest(), nodeIndex, graph, &dataToFill))
+            {
+                // the above function will emit an error if it fails.
+                return false;
+            }
+        }
+
+        bool allSuccess = true;
+        AZ_Info(
+            AZ::SceneAPI::Utilities::LogWindow,
+            "Generating UVs for " AZ_STRING_FORMAT ".\n",
+            AZ_STRING_ARG(currentNodeName));
+
+        switch (generationMethod)
+        {
+        case AZ::SceneAPI::DataTypes::UVsGenerationMethod::SphericalProjection:
+            {
+                allSuccess &= AZ::UVsGeneration::Mesh::SphericalMapping::GenerateUVsSphericalMapping(meshData, dataToFill);
+                break;
+            }
+            // for future expansion - add new methods here if you want to support additional methods of UV auto generation.
+        default:
+            {
+                AZ_Assert(false, "Unknown UVs generation method selected (%u) cannot generate UVs.\n", static_cast<AZ::u32>(generationMethod));
+                allSuccess = false;
+            }
+        }
+
+        return allSuccess;
+    }
+
+    size_t UVsGenerateComponent::CalcUvSetCount(
+        AZ::SceneAPI::Containers::SceneGraph& graph, const AZ::SceneAPI::Containers::SceneGraph::NodeIndex& nodeIndex) const
+    {
+        const auto nameContentView = AZ::SceneAPI::Containers::Views::MakePairView(graph.GetNameStorage(), graph.GetContentStorage());
+
+        size_t result = 0;
+        auto meshChildView = AZ::SceneAPI::Containers::Views::MakeSceneGraphChildView<AZ::SceneAPI::Containers::Views::AcceptEndPointsOnly>(
+            graph, nodeIndex, nameContentView.begin(), true);
+        for (auto child = meshChildView.begin(); child != meshChildView.end(); ++child)
+        {
+            AZ::SceneAPI::DataTypes::IMeshVertexUVData* data =
+                azrtti_cast<AZ::SceneAPI::DataTypes::IMeshVertexUVData*>(child->second.get());
+            if (data)
+            {
+                result++;
+            }
+        }
+
+        return result;
+    }
+
+    AZ::SceneAPI::DataTypes::IMeshVertexUVData* UVsGenerateComponent::FindUvData(
+        AZ::SceneAPI::Containers::SceneGraph& graph, const AZ::SceneAPI::Containers::SceneGraph::NodeIndex& nodeIndex, AZ::u64 uvSet) const
+    {
+        const auto nameContentView = AZ::SceneAPI::Containers::Views::MakePairView(graph.GetNameStorage(), graph.GetContentStorage());
+
+        AZ::u64 uvSetIndex = 0;
+        auto meshChildView = AZ::SceneAPI::Containers::Views::MakeSceneGraphChildView<AZ::SceneAPI::Containers::Views::AcceptEndPointsOnly>(
+            graph, nodeIndex, nameContentView.begin(), true);
+        for (auto child = meshChildView.begin(); child != meshChildView.end(); ++child)
+        {
+            AZ::SceneAPI::DataTypes::IMeshVertexUVData* data =
+                azrtti_cast<AZ::SceneAPI::DataTypes::IMeshVertexUVData*>(child->second.get());
+            if (data)
+            {
+                if (uvSetIndex == uvSet)
+                {
+                    return data;
+                }
+                uvSetIndex++;
+            }
+        }
+
+        return nullptr;
+    }
+
+    bool UVsGenerateComponent::CreateUVsLayer(
+        AZ::SceneAPI::Containers::SceneManifest& manifest,
+        const AZ::SceneAPI::Containers::SceneGraph::NodeIndex& nodeIndex,
+        AZ::SceneAPI::Containers::SceneGraph& graph,
+        SceneData::GraphData::MeshVertexUVData** outUVsData)
+    {
+        *outUVsData = nullptr;
+
+        AZStd::shared_ptr<SceneData::GraphData::MeshVertexUVData> uvData =
+            AZStd::make_shared<AZ::SceneData::GraphData::MeshVertexUVData>();
+
+        if (!uvData)
+        {
+            // it is unlikely you will even see this message since you are out of memory and the below
+            AZ_Error(AZ::SceneAPI::Utilities::ErrorWindow, false, "OUT OF MEMORY - Failed to allocate UV data.\n"
+            "You could try reducing the size of the files, splitting into multiple geometries, or reducing the number "
+            "or concurrent Asset Processor Jobs allowed to run.");
+            return false;
+        }
+
+       
+        const AZStd::string uvSetName =
+            AZ::SceneAPI::DataTypes::Utilities::CreateUniqueName<SceneData::GraphData::MeshVertexUVData>("UV0", manifest);
+        uvData->SetCustomName(uvSetName.c_str());
+
+        AZ::SceneAPI::Containers::SceneGraph::NodeIndex newIndex = graph.AddChild(nodeIndex, uvSetName.c_str(), uvData);
+        // if this assert triggers theres some terrible bug deep in the scene graph system, and the artist that sees it
+        // is not going to be able to fix it without code intervention (so assert).
+        AZ_Assert(newIndex.IsValid(), "Failed to create SceneGraph node for UVs attribute.");
+        if (!newIndex.IsValid())
+        {
+            return false;
+        }
+        graph.MakeEndPoint(newIndex);
+
+        *outUVsData = uvData.get();
+        return true;
+    }
+
+} // namespace AZ::SceneGenerationComponents

+ 54 - 0
Gems/SceneProcessing/Code/Source/Generation/Components/UVsGenerator/UVsGenerateComponent.h

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+#include <AzCore/RTTI/RTTI.h>
+#include <SceneAPI/SceneCore/Events/CallProcessorBus.h>
+#include <SceneAPI/SceneCore/Containers/Scene.h>
+
+namespace AZ
+{
+    namespace SceneAPI::DataTypes
+    {
+        class IMeshData;
+        class IMeshVertexUVData;
+    }
+
+    namespace SceneData::GraphData
+    {
+        class MeshVertexUVData;
+    }
+
+    class ComponentDescriptor;
+} // namespace AZ
+
+namespace AZ::SceneGenerationComponents
+{
+    struct UVsGenerateContext
+        : public AZ::SceneAPI::Events::ICallContext
+    {
+        AZ_RTTI(UVsGenerateContext, "{CC7301AB-A7EC-41FB-8BEE-DCC8C8C32BF4}", AZ::SceneAPI::Events::ICallContext)
+
+        UVsGenerateContext(AZ::SceneAPI::Containers::Scene& scene)
+            : m_scene(scene) {}
+        UVsGenerateContext& operator=(const UVsGenerateContext& other) = delete;
+
+        AZ::SceneAPI::Containers::Scene& GetScene() { return m_scene; }
+        const AZ::SceneAPI::Containers::Scene& GetScene() const { return m_scene; }
+
+    private:
+        AZ::SceneAPI::Containers::Scene& m_scene;
+    };
+
+    inline constexpr const char* s_UVsGenerateComponentTypeId = "{49121BDD-C7E5-4D39-89BC-28789C90057F}";
+
+    //! This function will be called by the module class to get the descriptor.  Doing it this way saves
+    //! it from having to actually see the entire component declaration here, it can all be in the implementation file.
+    AZ::ComponentDescriptor* CreateUVsGenerateComponentDescriptor();
+} // namespace AZ::SceneGenerationComponents

+ 54 - 0
Gems/SceneProcessing/Code/Source/Generation/Components/UVsGenerator/UVsGenerators/SphereMappingUVsGenerator.cpp

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Generation/Components/UVsGenerator/UVsGenerators/SphereMappingUVsGenerator.h>
+
+#include <SceneAPI/SceneCore/DataTypes/GraphData/IMeshData.h>
+#include <SceneAPI/SceneCore/Utilities/Reporting.h>
+#include <SceneAPI/SceneData/GraphData/MeshVertexUVData.h>
+#include <SceneAPI/SceneData/Rules/UVsRule.h>
+
+#include <AzCore/Math/Vector3.h>
+#include <AzCore/Math/Vector2.h>
+#include <AzCore/Math/Aabb.h>
+
+namespace AZ::UVsGeneration::Mesh::SphericalMapping
+{
+    bool GenerateUVsSphericalMapping(const AZ::SceneAPI::DataTypes::IMeshData* meshData, AZ::SceneData::GraphData::MeshVertexUVData* uvData)
+    {
+        uvData->Clear();
+        unsigned int vertexCount = meshData->GetVertexCount();
+        if (vertexCount == 0)
+        {
+            AZ_Trace(AZ::SceneAPI::Utilities::LogWindow, "Mesh has 0 vertex count, skipping UV generation.")
+            return true;
+        }
+
+        uvData->ReserveContainerSpace(meshData->GetVertexCount());
+
+        // calculate mesh center by bounding box center.
+        AZ::Aabb meshAabb = AZ::Aabb::CreateNull();
+        for (unsigned int vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex)
+        {
+            meshAabb.AddPoint(meshData->GetPosition(vertexIndex));
+        }
+
+        AZ::Vector3 centerPoint = meshAabb.GetCenter();
+        for (unsigned int vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex)
+        {
+            // project to sphere.
+            AZ::Vector3 projection = meshData->GetPosition(vertexIndex) - centerPoint;
+            projection.Normalize();
+            AZ::Vector2 uvCoords(asinf(projection.GetX()) / AZ::Constants::Pi + 0.5f,
+                                 asinf(projection.GetY()) / AZ::Constants::Pi + 0.5f);
+            uvData->AppendUV(uvCoords);
+        }
+
+        return true;
+    }
+} 

+ 22 - 0
Gems/SceneProcessing/Code/Source/Generation/Components/UVsGenerator/UVsGenerators/SphereMappingUVsGenerator.h

@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+namespace AZ::SceneAPI::DataTypes { class IMeshData; }
+namespace AZ::SceneData::GraphData { class MeshVertexUVData; } 
+
+namespace AZ::UVsGeneration::Mesh::SphericalMapping
+{
+    //! A simple positional sphere mapping UV generator.
+    //! The vertices from the mesh are projected onto a unit sphere and then that is used
+    //! to generate UV coordinates.
+    bool GenerateUVsSphericalMapping(
+        const AZ::SceneAPI::DataTypes::IMeshData* meshData, // in
+        AZ::SceneData::GraphData::MeshVertexUVData* uvData); // out
+}

+ 63 - 0
Gems/SceneProcessing/Code/Source/Generation/Components/UVsGenerator/UVsPreExportComponent.cpp

@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#include <Generation/Components/UVsGenerator/UVsPreExportComponent.h>
+
+#include <AzCore/RTTI/RTTI.h>
+
+#include <Generation/Components/UVsGenerator/UVsGenerateComponent.h> // for the context
+#include <SceneAPI/SceneCore/Components/GenerationComponent.h>
+#include <AzCore/Serialization/SerializeContext.h>
+#include <SceneAPI/SceneCore/Events/GenerateEventContext.h>
+#include <SceneAPI/SceneCore/Events/ProcessingResult.h>
+#include <SceneAPI/SceneCore/Events/CallProcessorBus.h>
+
+namespace AZ::SceneGenerationComponents
+{
+    //! This is the component responsible for actually hooking into the scene API's processing flow
+    //! during the generation step.
+    class UVsPreExportComponent : public AZ::SceneAPI::SceneCore::GenerationComponent
+    {
+    public:
+        AZ_COMPONENT(UVsPreExportComponent, s_UVsPreExportComponentTypeId, AZ::SceneAPI::SceneCore::GenerationComponent);
+        UVsPreExportComponent();
+
+        static void Reflect(AZ::ReflectContext* context);
+
+        AZ::SceneAPI::Events::ProcessingResult Register(AZ::SceneAPI::Events::GenerateAdditionEventContext& context);
+    };
+
+    AZ::ComponentDescriptor* CreateUVsPreExportComponentDescriptor()
+    {
+        return UVsPreExportComponent::CreateDescriptor();
+    }
+
+    namespace SceneEvents = AZ::SceneAPI::Events;
+
+    UVsPreExportComponent::UVsPreExportComponent()
+    {
+        BindToCall(&UVsPreExportComponent::Register);
+    }
+
+    void UVsPreExportComponent::Reflect(AZ::ReflectContext* context)
+    {
+        AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
+        if (serializeContext)
+        {
+            serializeContext->Class<UVsPreExportComponent, AZ::SceneAPI::SceneCore::GenerationComponent>()->Version(1);
+        }
+    }
+
+    AZ::SceneAPI::Events::ProcessingResult UVsPreExportComponent::Register(AZ::SceneAPI::Events::GenerateAdditionEventContext& context)
+    {
+        SceneEvents::ProcessingResultCombiner result;
+        UVsGenerateContext uvsGenerateContext(context.GetScene());
+        result += SceneEvents::Process<UVsGenerateContext>(uvsGenerateContext);
+        return result.GetResult();
+    }
+} // namespace AZ::SceneGenerationComponents

+ 23 - 0
Gems/SceneProcessing/Code/Source/Generation/Components/UVsGenerator/UVsPreExportComponent.h

@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) Contributors to the Open 3D Engine Project.
+ * For complete copyright and license terms please see the LICENSE at the root of this distribution.
+ *
+ * SPDX-License-Identifier: Apache-2.0 OR MIT
+ *
+ */
+
+#pragma once
+
+namespace AZ
+{
+    class ComponentDescriptor;
+}
+
+namespace AZ::SceneGenerationComponents
+{
+    inline constexpr const char* s_UVsPreExportComponentTypeId = "{64F79C1E-CED6-42A9-8229-6607F788C731}";
+
+    //! This function will be called by the module class to get the descriptor.  Doing it this way saves
+    //! it from having to actually see the entire component declaration here, it can all be in the implementation file.
+    AZ::ComponentDescriptor* CreateUVsPreExportComponentDescriptor();
+} // namespace AZ::SceneGenerationComponents

+ 4 - 0
Gems/SceneProcessing/Code/Source/SceneProcessingModule.cpp

@@ -16,6 +16,8 @@
 #include <Config/Widgets/GraphTypeSelector.h>
 #include <Generation/Components/TangentGenerator/TangentGenerateComponent.h>
 #include <Generation/Components/TangentGenerator/TangentPreExportComponent.h>
+#include <Generation/Components/UVsGenerator/UVsGenerateComponent.h>
+#include <Generation/Components/UVsGenerator/UVsPreExportComponent.h>
 #include <Generation/Components/MeshOptimizer/MeshOptimizerComponent.h>
 #include <Source/SceneProcessingModule.h>
 
@@ -44,6 +46,8 @@ namespace AZ
                     SceneBuilder::SceneSerializationHandler::CreateDescriptor(),
                     AZ::SceneGenerationComponents::TangentPreExportComponent::CreateDescriptor(),
                     AZ::SceneGenerationComponents::TangentGenerateComponent::CreateDescriptor(),
+                    AZ::SceneGenerationComponents::CreateUVsGenerateComponentDescriptor(),
+                    AZ::SceneGenerationComponents::CreateUVsPreExportComponentDescriptor(),
                     AZ::SceneGenerationComponents::MeshOptimizerComponent::CreateDescriptor(),
                 });
 

+ 6 - 0
Gems/SceneProcessing/Code/sceneprocessing_editor_static_files.cmake

@@ -20,6 +20,12 @@ set(FILES
     Source/Generation/Components/TangentGenerator/TangentGenerators/MikkTGenerator.cpp
     Source/Generation/Components/TangentGenerator/TangentGenerators/BlendShapeMikkTGenerator.h
     Source/Generation/Components/TangentGenerator/TangentGenerators/BlendShapeMikkTGenerator.cpp
+    Source/Generation/Components/UVsGenerator/UVsGenerateComponent.h
+    Source/Generation/Components/UVsGenerator/UVsGenerateComponent.cpp
+    Source/Generation/Components/UVsGenerator/UVsPreExportComponent.h
+    Source/Generation/Components/UVsGenerator/UVsPreExportComponent.cpp
+    Source/Generation/Components/UVsGenerator/UVsGenerators/SphereMappingUVsGenerator.h
+    Source/Generation/Components/UVsGenerator/UVsGenerators/SphereMappingUVsGenerator.cpp
     Source/Generation/Components/MeshOptimizer/MeshBuilder.cpp
     Source/Generation/Components/MeshOptimizer/MeshBuilder.h
     Source/Generation/Components/MeshOptimizer/MeshBuilderInvalidIndex.h

+ 46 - 38
Registry/sceneassetimporter.setreg

@@ -1,39 +1,47 @@
 {
-	"O3DE":
-	{
-		"SceneAPI":
-		{
-			"AssetImporter":
-			{
-				"SupportedFileTypeExtensions":
-				[
-					".fbx",
-					".stl",
-					".gltf",
-					".glb"
-				]
-			},
-			"MaterialConverter": 
-			{
-				"Enable": true,
-				"DefaultMaterial": "Materials/Presets/PBR/default_grid.material"
-			},
-			"TangentGenerateComponent":
-			{
-				"DefaultGenerationMethod": "FromSourceScene",
-				"DebugBitangentFlip": false
-			},
-			"SkinRule":
-			{
-				"DefaultMaxSkinInfluencesPerVertex": 8,
-				"DefaultWeightThreshold": 0.001
-			},
-			"ModelBuilder": 
-			{
-				// When false, scenes with multiple meshes assigned to the same material but with different vertex layouts will successfully process
-				// When true, stricter error checking will cause the asset to fail to process with an error message indicating why
-				"MismatchedVertexLayoutsAreErrors": false 
-			}
-		}
-	}
-}
+    "O3DE":
+    {
+        "SceneAPI":
+        {
+            "AssetImporter":
+            {
+                "SupportedFileTypeExtensions":
+                [
+                    ".fbx",
+                    ".stl",
+                    ".gltf",
+                    ".glb"
+                ]
+            },
+            "MaterialConverter": 
+            {
+                "Enable": true,
+                "DefaultMaterial": "Materials/Presets/PBR/default_grid.material"
+            },
+            "TangentGenerateComponent":
+            {
+                "DefaultGenerationMethod": "FromSourceScene",
+                "DebugBitangentFlip": false
+            },
+            "UVsGenerateComponent":
+            {
+                // you may have to reprocess assets if you change these defaults.
+                // When the user has not added any rules regarding UV generation, what should we do? Choose from "SphericalProjection" or "LeaveSceneDataAsIs"
+                "DefaultGenerationMethodIfNoRulePresent" : "LeaveSceneDataAsIs",
+                // when the user actually clicks to add a new Rule or a script adds a new rule to generate uvs, what method should it use by default?
+                "DefaultGenerationMethodWhenRuleIsPresent" : "SphericalProjection"
+            },
+            "SkinRule":
+            {
+                "DefaultMaxSkinInfluencesPerVertex": 8,
+                "DefaultWeightThreshold": 0.001
+            },
+            "ModelBuilder": 
+            {
+                // When false, scenes with multiple meshes assigned to the same material but with different vertex layouts will successfully process
+                // When true, stricter error checking will cause the asset to fail to process with an error message indicating why
+                "MismatchedVertexLayoutsAreErrors": false 
+            }
+        }
+    }
+}