Initial commit

This commit is contained in:
alexpete
2021-03-05 11:26:34 -08:00
commit a10351f38d
27091 changed files with 5521199 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
add_subdirectory(Code)
@@ -0,0 +1,153 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <SceneAPI/SceneCore/Containers/Scene.h>
#include <SceneAPI/SceneCore/Containers/SceneManifest.h>
#include <SceneAPI/SceneCore/Containers/Utilities/Filters.h>
#include <SceneAPI/SceneCore/Utilities/Reporting.h>
#include <Behaviors/LoggingGroupBehavior.h>
#include <Groups/LoggingGroup.h>
namespace SceneLoggingExample
{
// Reflection is a basic requirement for components. For behaviors, you can often keep the Reflect()
// function simple because the SceneAPI just needs to be able to find the component. For more details
// on the Reflect() function, see LoggingGroup.cpp.
void LoggingGroupBehavior::Reflect(AZ::ReflectContext* context)
{
// The data and UI elements used in the SceneAPI are not components, but they need to be reflected
// for serialization and the Scene Settings to work. This can done at any point in the gem, but the
// behavior that controls the data is a good place for this. Because the LoggingGroupBehavior controls
// the LoggingGroup, we will register it here.
LoggingGroup::Reflect(context);
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<LoggingGroupBehavior, AZ::SceneAPI::SceneCore::BehaviorComponent>()->Version(1);
}
}
// Later in this example, messages that deal with manifest changes and loading files will be used
// to create the various ways that the behavior controls settings. Before any events can be sent
// to the behavior, it first needs to be connected to the EBuses that it monitors.
void LoggingGroupBehavior::Activate()
{
AZ::SceneAPI::Events::ManifestMetaInfoBus::Handler::BusConnect();
AZ::SceneAPI::Events::AssetImportRequestBus::Handler::BusConnect();
}
// Disconnect from the EBuses when this behavior is no longer active.
void LoggingGroupBehavior::Deactivate()
{
AZ::SceneAPI::Events::ManifestMetaInfoBus::Handler::BusDisconnect();
AZ::SceneAPI::Events::AssetImportRequestBus::Handler::BusDisconnect();
}
// This behavior will control the logging for the UI, so let's begin by registering the LoggingGroup with the UI under a new
// "Logging" tab and ignore the position of the tab for now. This will add a new tab to the Scene Settings window. The tab
// will have a single button to add a LoggingGroup. If additional groups are registered under the same tab name, the button
// will be changed to a drop-down button and allow the registered groups to be added.
//
// The scene is passed as one of the arguments so that the manifest and/or the graph can be inspected to determine if a group
// should be added. For example, if the graph doesn't contain any meshes, the mesh group can be left out. This helps prevent
// users from adding groups that have no effect.
void LoggingGroupBehavior::GetCategoryAssignments(CategoryRegistrationList& categories, [[maybe_unused]] const AZ::SceneAPI::Containers::Scene& scene)
{
categories.emplace_back("Logging", LoggingGroup::TYPEINFO_Uuid());
}
// When a scene is loaded for the first time (for example, from an .fbx file), there won't be a manifest (.assetinfo file).
// If the scene was loaded previously, there might be a manifest that requires updates because it contains values that no
// longer match the graph. This EBus call gives a one-time opportunity right after loading has completed to update the manifest
// or to add data to a new one.
//
// In this example, let's add a LoggingGroup to a new manifest only. Don't forget to remove the manifest (.assetinfo file)
// for your test scene file. Otherwise, the following code won't trigger.
AZ::SceneAPI::Events::ProcessingResult LoggingGroupBehavior::UpdateManifest(AZ::SceneAPI::Containers::Scene& scene,
ManifestAction action, [[maybe_unused]] RequestingApplication requester)
{
if (action == ManifestAction::ConstructDefault)
{
AZStd::shared_ptr<LoggingGroup> group = AZStd::make_shared<LoggingGroup>();
// This might not be the only behavior that wants to make modifications to the new group. An example is a material
// behavior that wants to add a material rule when a mesh group is created. By calling the EBus below, other behaviors
// get a chance to change or add their own values. Listening to this EBus is also a good place to add any settings to
// the new group instead of doing it here. This is because this EBus is also called when tools such as the UI create
// a new group, which keeps initialization in one place.
AZ::SceneAPI::Events::ManifestMetaInfoBus::Broadcast(
&AZ::SceneAPI::Events::ManifestMetaInfoBus::Events::InitializeObject, scene, *group);
if (scene.GetManifest().AddEntry(AZStd::move(group)))
{
// Let the SceneAPI know that a LoggingGroup has been successfully added.
return AZ::SceneAPI::Events::ProcessingResult::Success;
}
else
{
// It wasn't possible to add the new logging group, so let the SceneAPI know that
// a problem was encountered. Don't forget to also tell the user what is going on,
// because this will cause the loading to fail.
AZ_TracePrintf(AZ::SceneAPI::Utilities::ErrorWindow, "Unable to add a new logging group.");
return AZ::SceneAPI::Events::ProcessingResult::Failure;
}
}
// In any other situation, there's no plan to do anything so tell the SceneAPI to ignore this behavior.
return AZ::SceneAPI::Events::ProcessingResult::Ignored;
}
// When a new manifest object is created, the caller can choose to allow other behaviors to change or add their own data, such
// as rules to a group. The EBus call in the above function shows a typical use case. Using InitializeObject() provides a more
// powerful alternative to default values. It allows domain logic to be spread to appropriate behaviors, but also allows general
// awareness of the manifest and graph to select default values that are more appropriate to the user.
//
// For this example, let's use the passed-in manifest to look for the last LoggingGroup in the manifest and use the log setting that
// is its opposite. When viewing this in the Scene Settings window, "Log processing events" will be off when adding a new logging group.
// The one directly above it is on, and vice versa.
void LoggingGroupBehavior::InitializeObject(const AZ::SceneAPI::Containers::Scene& scene, AZ::SceneAPI::DataTypes::IManifestObject& target)
{
// If the item being added isn't a LoggingGroup, ignore it.
if (!target.RTTI_IsTypeOf(LoggingGroup::TYPEINFO_Uuid()))
{
return;
}
LoggingGroup* newGroup = azrtti_cast<LoggingGroup*>(&target);
AZ_Assert(newGroup, "Manifest object has been identified as LoggingGroup, but failed to cast to it.");
// First create a view that only contains instances that exactly match LoggingGroups. Use MakeDerivedFilterView() to do
// the same for any instances that implement a specific interface and/or base class. For more details on using iterators
// to get data from the manifest and graph, see ExportTrackingProcessor.cpp.
auto values = scene.GetManifest().GetValueStorage();
auto view = AZ::SceneAPI::Containers::MakeExactFilterView<LoggingGroup>(values);
// Find the last LoggingGroup in the manifest.
auto last = view.begin();
while (AZStd::next(last) != view.end())
{
++last;
}
// Only take the values if there's actually another LoggingGroup in the manifest.
if (last != view.end())
{
newGroup->ShouldLogProcessingEvents(!last->DoesLogProcessingEvents());
}
// Let's also set a default name for this group. Groups often match one-to-one with the file that they output.
// For example, a Mesh Group will produce a .cgf file with the same name. If the name is used as a file name,
// it is important to check whether it's a valid path name and isn't duplicating another name.
const size_t size = AZStd::distance(view.begin(), view.end());
newGroup->SetName(AZStd::string::format("Logger_%zu", size));
}
} // namespace SceneLoggingExample
@@ -0,0 +1,44 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <SceneAPI/SceneCore/Components/BehaviorComponent.h>
#include <SceneAPI/SceneCore/Events/ManifestMetaInfoBus.h>
#include <SceneAPI/SceneCore/Events/AssetImportRequest.h>
namespace SceneLoggingExample
{
// The LoggingGroupBehavior shows how a behavior can be written that monitors
// manifest activity and reacts to it in order to setup default values for
// manifest entries. It also demonstrates how to register new UI elements with
// the SceneAPI.
class LoggingGroupBehavior
: public AZ::SceneAPI::SceneCore::BehaviorComponent
, public AZ::SceneAPI::Events::ManifestMetaInfoBus::Handler
, public AZ::SceneAPI::Events::AssetImportRequestBus::Handler
{
public:
AZ_COMPONENT(LoggingGroupBehavior, "{4DE18DD7-5C40-4A14-8CD7-67162171DCAA}", AZ::SceneAPI::SceneCore::BehaviorComponent);
~LoggingGroupBehavior() override = default;
void Activate();
void Deactivate();
static void Reflect(AZ::ReflectContext* context);
void GetCategoryAssignments(CategoryRegistrationList& categories, const AZ::SceneAPI::Containers::Scene& scene) override;
AZ::SceneAPI::Events::ProcessingResult UpdateManifest(AZ::SceneAPI::Containers::Scene& scene,
ManifestAction action, RequestingApplication requester) override;
void InitializeObject(const AZ::SceneAPI::Containers::Scene& scene, AZ::SceneAPI::DataTypes::IManifestObject& target) override;
};
} // namespace SceneLoggingExample
@@ -0,0 +1,47 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
if (NOT PAL_TRAIT_BUILD_HOST_TOOLS)
return()
endif()
ly_add_target(
NAME SceneLoggingExample.Static STATIC
NAMESPACE Gem
FILES_CMAKE
sceneloggingexample_files.cmake
INCLUDE_DIRECTORIES
PRIVATE
.
PUBLIC
Include
BUILD_DEPENDENCIES
PUBLIC
AZ::AzCore
AZ::SceneCore
Legacy::CryCommon
)
ly_add_target(
NAME SceneLoggingExample ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE}
NAMESPACE Gem
OUTPUT_NAME Gem.SceneLoggingExample.35d8f6e49ae04c9382c61a42d4355c2f.v0.1.0
FILES_CMAKE
sceneloggingexample_shared_files.cmake
INCLUDE_DIRECTORIES
PRIVATE
.
PUBLIC
Include
BUILD_DEPENDENCIES
PRIVATE
Gem::SceneLoggingExample.Static
)
@@ -0,0 +1,147 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <Groups/LoggingGroup.h>
namespace SceneLoggingExample
{
const char* LoggingGroup::s_disabledOption = "No logging";
LoggingGroup::LoggingGroup()
: m_logProcessingEvents(true)
{
}
// The data in groups will be saved to the manifest file and will be reflected in the Scene Settings
// window. For those systems to do their work, the LoggingGroup needs to tell a bit more about itself
// than the other classes in this example.
void LoggingGroup::Reflect(AZ::ReflectContext* context)
{
// There are different kind of contexts, but for groups and rules, the only one that's
// interesting is the SerializeContext. Check if the provided context is one.
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (!serializeContext)
{
return;
}
// Next, specify the fields that need to be serialized to and from a manifest. This allows new
// fields to be stored and loaded from the manifest (.assetinfo file). These are also needed
// for the edit context below.
serializeContext->Class<LoggingGroup, AZ::SceneAPI::DataTypes::IManifestObject>()->Version(1)
->Field("groupName", &LoggingGroup::m_groupName)
->Field("graphLogRoot", &LoggingGroup::m_graphLogRoot)
->Field("logProcessingEvents", &LoggingGroup::m_logProcessingEvents);
// The EditContext allows you to add additional meta information to the previously registered fields.
// This meta information will be used in the Scene Settings, which uses the Reflected Property Editor.
AZ::EditContext* editContext = serializeContext->GetEditContext();
if (editContext)
{
editContext->Class<LoggingGroup>("Logger", "Add additional logging to the SceneAPI.")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute("AutoExpand", true)
->Attribute(AZ::Edit::Attributes::NameLabelOverride, "")
->DataElement(AZ::Edit::UIHandlers::Default, &LoggingGroup::m_groupName,
"Name", "The name of the group will be used in the log")
// The Reflected Property Editor can pick a default editor for many types. However, for the string
// that will store the selected node, a more specialized editor is needed. NodeListSelection is
// one such editor and it is SceneGraph-aware. It allows the selection of a specific node from the
// graph and the selectable items can be filtered. You can find other available editors in the
// "RowWidgets"-folder of the SceneUI.
->DataElement(AZ_CRC("NodeListSelection", 0x45c54909), &LoggingGroup::m_graphLogRoot,
"Graph log root", "Select the node in the graph to list children of to the log, or disable logging.")
->Attribute(AZ_CRC("DisabledOption", 0x6cd17278), s_disabledOption)
// Nodes in the SceneGraph can be marked as endpoints. To the graph, this means that these nodes
// are not allowed to have children. While not a true one-to-one mapping, endpoints often act as
// attributes to a node. For example, a transform can be marked as an endpoint. This means that
// it applies its transform to the parent object like an attribute. If the transform is not marked
// as an endpoint, then it is the root transform for the group(s) that are its children.
->Attribute(AZ_CRC("ExcludeEndPoints", 0x53bd29cc), true)
->DataElement(AZ::Edit::UIHandlers::Default, &LoggingGroup::m_logProcessingEvents,
"Log processing events", "Log processing events as they happen.");
}
}
void LoggingGroup::SetName(const AZStd::string& name)
{
m_groupName = name;
}
void LoggingGroup::SetName(AZStd::string&& name)
{
m_groupName = AZStd::move(name);
}
const AZStd::string& LoggingGroup::GetName() const
{
return m_groupName;
}
// Groups need to provide a unique id that will be used to create the final sub id for the product
// build using this group. While new groups created through the UI can remain fully random, it's
// important that ids used for defaults are recreated the same way every time. It's recommended this
// is done by using the source guid of the file and calling DataTypes::Utilities::CreateStableUuid.
// If the id doesn't remain stable between updates this will cause the sub id to change which will in
// turn cause the objects links to those products to break.
//
// As this example doesn't have a product, the id is not important so just always return the randomly
// generated id.
const AZ::Uuid& LoggingGroup::GetId() const
{
return m_id;
}
// Groups have the minimal amount of options to generate a working product in the cache and nothing more.
// A group might not be perfect or contain all the data the user would expect, but it will load in the
// engine and not crash. You can add additional settings to fine tune the exporting process in the form of
// rules (or "modifiers" in the Scene Settings UI). Rules usually group a subset of settings together,
// such as control of physics or level of detail. This approach keeps UI clutter to a minimum by only
// presenting options that are relevant for the user's file, while still providing access to all available
// settings.
//
// By using the "GetAvailableModifiers" in the ManifestMetaInfoHandler EBus, it's possible to filter out
// any options that are not relevant to the group. For example, if a group only allows for a single instance
// of a rule, it would no longer be added to this call if there is already one. Because the logging doesn't
// require any rules, empty defaults are provided.
AZ::SceneAPI::Containers::RuleContainer& LoggingGroup::GetRuleContainer()
{
return m_ruleContainer;
}
const AZ::SceneAPI::Containers::RuleContainer& LoggingGroup::GetRuleContainerConst() const
{
return m_ruleContainer;
}
const AZStd::string& LoggingGroup::GetGraphLogRoot() const
{
return m_graphLogRoot;
}
bool LoggingGroup::DoesLogGraph() const
{
return m_graphLogRoot.compare(s_disabledOption) != 0;
}
bool LoggingGroup::DoesLogProcessingEvents() const
{
return m_logProcessingEvents;
}
void LoggingGroup::ShouldLogProcessingEvents(bool state)
{
m_logProcessingEvents = state;
}
} // namespace SceneLoggingExample
@@ -0,0 +1,70 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/std/string/string.h>
#include <AzCore/Memory/Memory.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <SceneAPI/SceneCore/DataTypes/Groups/IGroup.h>
namespace AZ
{
class ReflectContext;
}
namespace SceneLoggingExample
{
// The LoggingGroup class contains the settings that will be interpreted by the exporter.
// Groups typically contain settings and information only. They rarely implement any advanced logic.
//
// Groups tend to have a one-to-one relationship to the target format. For example, every mesh group
// will produce a .cgf file in the cache. Groups also aim to be the most basic form of the required data,
// providing the minimum information that is needed to create a valid product in the cache.
//
// To further fine tune the group, you can add rules (also called modifiers). For example, you can add a rule to
// control the world matrix.
class LoggingGroup : public AZ::SceneAPI::DataTypes::IGroup
{
public:
LoggingGroup();
~LoggingGroup() override = default;
AZ_RTTI(LoggingGroup, "{A5ECF95D-2E84-4574-BF93-09E469E2BA4E}", AZ::SceneAPI::DataTypes::IGroup);
AZ_CLASS_ALLOCATOR(LoggingGroup, AZ::SystemAllocator, 0);
static void Reflect(AZ::ReflectContext* context);
void SetName(const AZStd::string& name);
void SetName(AZStd::string&& name);
const AZStd::string& GetName() const override;
const AZ::Uuid& GetId() const override;
AZ::SceneAPI::Containers::RuleContainer& GetRuleContainer() override;
const AZ::SceneAPI::Containers::RuleContainer& GetRuleContainerConst() const override;
const AZStd::string& GetGraphLogRoot() const;
bool DoesLogGraph() const;
bool DoesLogProcessingEvents() const;
void ShouldLogProcessingEvents(bool state);
protected:
AZ::SceneAPI::Containers::RuleContainer m_ruleContainer;
AZStd::string m_groupName;
AZStd::string m_graphLogRoot;
AZ::Uuid m_id;
bool m_logProcessingEvents;
static const char* s_disabledOption;
};
} // namespace SceneLoggingExample
@@ -0,0 +1,192 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <SceneAPI/SceneCore/Containers/Scene.h>
#include <SceneAPI/SceneCore/Containers/SceneManifest.h>
#include <SceneAPI/SceneCore/Containers/Utilities/Filters.h>
#include <SceneAPI/SceneCore/Containers/Views/PairIterator.h>
#include <SceneAPI/SceneCore/Containers/Views/SceneGraphDownwardsIterator.h>
#include <SceneAPI/SceneCore/Utilities/Reporting.h>
#include <Processors/ExportTrackingProcessor.h>
#include <Groups/LoggingGroup.h>
namespace SceneLoggingExample
{
ExportTrackingProcessor::ExportTrackingProcessor()
{
// The scene conversion and exporting process uses the CallProcessorBus to move data and trigger additional work.
// The CallProcessorBus operates differently than typical EBuses because it doesn't have a specific set of functions
// that you can call. Instead, it works like a pseudo-remote procedure call, where the arguments for what would
// normally be a function are stored in a context.
//
// The CallProcessorBus provides a single place to register and trigger the context calls. Based on the type
// of context, the appropriate functionality is executed. To make it easier to work with, a binding layer
// called CallProcessorBinder allows binding to a function that takes a context as an argument and performs
// all the routing. One of the benefits of this approach is that it provides several places to hook custom code
// into without having to update existing code. For example, you can use this approach to write additional information
// to a mesh file without having to change how the .cgf exporter works.
//
// The example below attaches the PrepareForExport function to the PreExportEventContext so that this context
// is sent to the CallProcessorBus at the start of every conversion and export process.
BindToCall(&ExportTrackingProcessor::PrepareForExport);
// By default, the CallProcessorBinder will only activate if the context exactly matches the argument of the
// bound function. That setup is often desired to avoid receiving many unrelated events. However, this example
// uses "Derived" and binds to the ICallContext so that all events are printed. Note that many events get fired
// multiple times due to multiple phases (pre, active, and post).
BindToCall(&ExportTrackingProcessor::ContextCallback, AZ::SceneAPI::Events::CallProcessorBinder::TypeMatch::Derived);
}
// Reflection is a basic requirement for components. For Exporting components, you can often keep the Reflect() function
// simple because the SceneAPI just needs to be able to find the component. For more details on the Reflect() function,
// see LoggingGroup.cpp.
void ExportTrackingProcessor::Reflect(AZ::ReflectContext* context)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<ExportTrackingProcessor, AZ::SceneAPI::SceneCore::ExportingComponent>()->Version(1);
}
}
// This function is now bound to the CallProcessorBinder, so it will be called as soon as exporting starts. It is a good point
// at which to look at the available groups and see if there are groups that need to log the scene graph.
AZ::SceneAPI::Events::ProcessingResult ExportTrackingProcessor::PrepareForExport(AZ::SceneAPI::Events::PreExportEventContext& context)
{
// Before doing any work, the manifest must be searched for instructions about what needs to be done. The instructions
// are in the form of groups and rules. In this example, we use this opportunity to log the scene graphs that are
// listed in every logging group.
//
// In this example, the manifest is cached for later use. This is typically not recommended because multiple builders can
// be running at the same time, resulting in callbacks from multiple exports that are in flight. In general, you should
// pass in any required information as a member of the context.
m_manifest = &context.GetScene().GetManifest();
// The manifest is a flat list of IManifestObjects and relies on AZ_RTTI to determine its content. Content can be retrieved
// through an index-based approach or an iterator approach. The index-based approach tends to be easier to understand but
// it also requires you to work with more code. The iterator has more complex syntax and can produce more complicated compile
// errors, but it has several utilities that make it more concise to work with and often makes code that better communicates
// intention. To provide examples of both cases, the index-based approach is used below, and the iterator approach is used in
// the ContextCallback function.
size_t count = m_manifest->GetEntryCount();
for (size_t i = 0; i < count; ++i)
{
AZStd::shared_ptr<const AZ::SceneAPI::DataTypes::IManifestObject> entry = m_manifest->GetValue(i);
// The azrtti_cast is a run-time type-aware cast that will return a nullptr if the provided type
// can't be cast to the target class. That principle is used here to filter for LoggingGroups only.
const LoggingGroup* group = azrtti_cast<const LoggingGroup*>(entry.get());
if (group)
{
if (group->DoesLogGraph())
{
// For every group, write out the graph information, starting at the node the user selected.
LogGraph(context.GetScene().GetGraph(), group->GetGraphLogRoot());
}
}
}
return AZ::SceneAPI::Events::ProcessingResult::Success;
}
// In the constructor, this function was bound to accept any contexts that are derived from ICallContext, which is the base
// for all CallProcessorBus events. This allows for monitoring of everything that happens during conversion and exporting.
AZ::SceneAPI::Events::ProcessingResult ExportTrackingProcessor::ContextCallback([[maybe_unused]] AZ::SceneAPI::Events::ICallContext& context)
{
// PrepareForExport demonstrated getting data from the manifest using the index-based approach. The code below demonstrates the
// iterator approach by getting a view (a begin- and end-iterator) and creating a filtered view on top of it.
auto manifestValues = m_manifest->GetValueStorage();
auto view = AZ::SceneAPI::Containers::MakeExactFilterView<LoggingGroup>(manifestValues);
// Now that the filtered view of the manifest is constructed, the loop below will list only LoggingGroups. Groups typically
// map one-to-one to an output file. This is not a hard requirement, but it is most often the case. In that case, it is typical
// for multiple groups to be individually exported to their own file. Most groups will also have rules (also called modifiers)
// that add fine-grained control to the conversion process. Usually this is in one particular area such as the world matrix or physics.
for (const auto& it : view)
{
if (it.DoesLogProcessingEvents())
{
AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "ExportEvent (%s): %s", it.GetName().c_str(), context.RTTI_GetTypeName());
}
}
return AZ::SceneAPI::Events::ProcessingResult::Ignored;
}
// With the SceneAPI, the order in which an EBus calls its listeners is mostly random. This generally isn't a problem because most work
// is done in isolation. If there is a dependency, we recommend that you break a call into multiple smaller calls, but this isn't always
// an option. For example, perhaps there is no source code available for third-party extensions or you are trying to avoid making code
// changes to the engine/editor. For those situations, the Call Processor allows you to specify a priority to make sure that a call is made
// before or after all other listeners have done their work.
//
// In this example, we want the log messages to be printed before any other listeners do their work and potentially print their data.
// To accomplish this, we set the priority to the highest available number.
uint8_t ExportTrackingProcessor::GetPriority() const
{
return EarliestProcessing;
}
// During the loading process, an in-memory representation of the scene is stored inside the SceneGraph. The SceneCore library
// provides several interfaces that you can use as a basis for data that helps establish a common vocabulary for the various parts
// of the SceneAPI. The SceneData library provides an optional set of implementations of these interfaces for your convenience. Similar
// to the manifest, the SceneGraph can provide its data through an index-based or an iterator-based approach.
void ExportTrackingProcessor::LogGraph(const AZ::SceneAPI::Containers::SceneGraph& graph, const AZStd::string& nodePath) const
{
namespace SceneViews = AZ::SceneAPI::Containers::Views;
// Between runs, the source scene files (for example, .fbx) can change. Storing indices to nodes can lead to unexpected behavior,
// so it is generally preferable to store the node path instead. This makes looking up nodes by name a common pattern. Rather than
// doing a linear search over the names, the SceneGraph has an optimized lookup of the node name.
AZ::SceneAPI::Containers::SceneGraph::NodeIndex nodeIndex = graph.Find(nodePath);
if (!nodeIndex.IsValid())
{
// Any SceneGraph is guaranteed to have at least a root node, even if it is otherwise empty. Note that not all loaders
// may choose to use this node. This can occasionally lead to an unexpected node at the top of the graph.
nodeIndex = graph.GetRoot();
}
// The SceneGraph stores its data in separate containers, such as a content list and a name list. The relationship between nodes is
// stored in a similar flat list. This allows iterating over the content in both a hierarchical and a linear way. Because hierarchical
// traversal is much more expensive than linear traversal, questions such as "list all entries of type X" are answered much more efficiently
// by using linear traversal.
auto nameStorage = graph.GetNameStorage();
auto contentStorage = graph.GetContentStorage();
// As described previously, the name and content of the graph are stored separately. However, sometimes both are needed when traversing
// the graph. To combine the two in a single iterator, you can use the pair iterator in the following way.
auto nameContentView = SceneViews::MakePairView(nameStorage, contentStorage);
// The SceneGraph has several iterators that help with traversing the graph in a hierarchical way:
// - SceneGraphUpwardsIterator - Traverses from a given node to the root of the graph.
// - SceneGraphDownwardsIterator - Traverses over all children of a given node either breadth-first or depth-first.
// - SceneGraphChildIterator - Traverses over the direct children of a node only.
// For this example, all nodes beneath the node that the user selected are listed so a downwards iterator is most appropriate.
auto graphDownwardsView = SceneViews::MakeSceneGraphDownwardsView<SceneViews::BreadthFirst>(graph, nodeIndex, nameContentView.begin(), true);
for (auto it = graphDownwardsView.begin(); it != graphDownwardsView.end(); ++it)
{
const char* path = it->first.GetPath();
const char* type = it->second ? it->second->RTTI_GetTypeName() : "No data";
// While it's generally preferable to stick with either index- or iterator-based traversal, there may be times where switching between one
// or the other becomes necessary. The SceneGraph provides utility functions to convert between the two approaches.
AZ::SceneAPI::Containers::SceneGraph::NodeIndex itNodeIndex = graph.ConvertToNodeIndex(it.GetHierarchyIterator());
// Nodes in the SceneGraph can be marked as endpoints. To the graph, this means that these nodes are not allowed to have children.
// While not a true one-to-one mapping, endpoints often act as attributes to a node. For example, a transform can be marked as an endpoint.
// This means that it applies its transform to the parent object like an attribute. If the transform is not marked as an endpoint, then it
// is the root transform for the group(s) that are its children.
bool isEndPoint = graph.IsNodeEndPoint(itNodeIndex);
AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "'%s' '%s' contains data of type '%s'.", (isEndPoint ? "End point node" : "Node"), path, type);
}
}
} // namespace SceneLoggingExample
@@ -0,0 +1,57 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <AzCore/std/string/string.h>
#include <SceneAPI/SceneCore/Components/ExportingComponent.h>
#include <SceneAPI/SceneCore/Events/ExportEventContext.h>
namespace AZ
{
namespace SceneAPI
{
namespace Containers
{
class Scene;
class SceneGraph;
class SceneManifest;
}
}
}
namespace SceneLoggingExample
{
// The ExportTrackingProcessor class demonstrates how to use the ExportingComponent to listen to scene export events.
// It also shows how to collect data from a graph by traversing the graph in a hierarchical way.
class ExportTrackingProcessor
: public AZ::SceneAPI::SceneCore::ExportingComponent
{
public:
AZ_COMPONENT(ExportTrackingProcessor, "{EAD9C07A-60D5-4E48-8465-72034D326368}", AZ::SceneAPI::SceneCore::ExportingComponent);
ExportTrackingProcessor();
~ExportTrackingProcessor() override = default;
static void Reflect(AZ::ReflectContext* context);
protected:
AZ::SceneAPI::Events::ProcessingResult PrepareForExport(AZ::SceneAPI::Events::PreExportEventContext& context);
AZ::SceneAPI::Events::ProcessingResult ContextCallback(AZ::SceneAPI::Events::ICallContext& context);
uint8_t GetPriority() const override;
void LogGraph(const AZ::SceneAPI::Containers::SceneGraph& graph, const AZStd::string& nodePath) const;
const AZ::SceneAPI::Containers::SceneManifest* m_manifest = nullptr;
};
} // namespace SceneLoggingExample
@@ -0,0 +1,134 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/Serialization/SerializeContext.h>
#include <Processors/LoadingTrackingProcessor.h>
#include <SceneAPI/SceneCore/Utilities/Reporting.h>
namespace SceneLoggingExample
{
LoadingTrackingProcessor::LoadingTrackingProcessor()
{
// For details about the CallProcessorBus and CallProcessorBinder, see ExportTrackingProcessor.cpp.
BindToCall(&LoadingTrackingProcessor::ContextCallback, AZ::SceneAPI::Events::CallProcessorBinder::TypeMatch::Derived);
}
// Reflection is a basic requirement for components. For Loading components, you can often keep the Reflect() function
// simple because the SceneAPI just needs to be able to find the component. For more details on the Reflect() function,
// see LoggingGroup.cpp.
void LoadingTrackingProcessor::Reflect(AZ::ReflectContext* context)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<LoadingTrackingProcessor, AZ::SceneAPI::SceneCore::LoadingComponent>()->Version(1);
}
}
// Later in this example, we will listen to and log messages that relate to file loading.
// Before this can happen, we must connect to the bus that sends the messages.
void LoadingTrackingProcessor::Activate()
{
AZ::SceneAPI::Events::AssetImportRequestBus::Handler::BusConnect();
// Forward the call to the LoadingComponent so that the call bindings get activated.
AZ::SceneAPI::SceneCore::LoadingComponent::Activate();
}
// Disconnect from the bus upon deactivation.
void LoadingTrackingProcessor::Deactivate()
{
AZ::SceneAPI::SceneCore::LoadingComponent::Deactivate();
// Forward the call to the LoadingComponent so that the call bindings get deactivated.
AZ::SceneAPI::Events::CallProcessorBus::Handler::BusDisconnect();
AZ::SceneAPI::Events::AssetImportRequestBus::Handler::BusDisconnect();
}
// Loading starts by announcing that loading will begin shortly. This provides an opportunity to prepare
// caches or to take any additional steps that are required before loading.
AZ::SceneAPI::Events::ProcessingResult LoadingTrackingProcessor::PrepareForAssetLoading(AZ::SceneAPI::Containers::Scene& /*scene*/,
RequestingApplication /*requester*/)
{
AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "Preparing to load a scene.");
// This function doesn't contribute anything to the loading, so let the SceneAPI know that it can ignore its contributions.
return AZ::SceneAPI::Events::ProcessingResult::Ignored;
}
// After a call to PrepareForAssetLoading has been dispatched, the scene file (for example, .fbx) will be loaded.
// This is normally what scene builders will be looking for. If the file has an extension that a scene builder
// understands, it will start reading the source file, convert the data, and store it in the scene. This is also
// true for loading the manifest file, which happens in this same pass.
//
// For this example, nothing is done because there's no data to read. We just echo the steps that are taken.
AZ::SceneAPI::Events::LoadingResult LoadingTrackingProcessor::LoadAsset(AZ::SceneAPI::Containers::Scene& /*scene*/,
[[maybe_unused]] const AZStd::string& path, const AZ::Uuid& /*guid*/, RequestingApplication /*requester*/)
{
AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "Loading scene from '%s'.", path.c_str());
return AZ::SceneAPI::Events::LoadingResult::Ignored;
}
// After the scene file and manifest are loaded, we finalize the loading by making two calls: first to FinalizeAssetLoading()
// and then to UpdateManifest().
//
// FinalizeAssetLoading() is the best time to close out any temporary buffers, clear cache, patch pointers, and any
// other final steps that are required to put the graph in a valid state and perform any necessary cleanup.
// We also disconnect from the Call Processor bus so that we won't receive export events later. It is possible to
// make updates to the manifest in FinalizeAssetLoading(), but UpdateManifest() is a better place to do this.
void LoadingTrackingProcessor::FinalizeAssetLoading(AZ::SceneAPI::Containers::Scene& /*scene*/, RequestingApplication /*requester*/)
{
AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "Finished loading scene.");
}
// UpdateManifest() provides additional information about the state of the manifest, such as if a default manifest is being
// built or an existing one is being updated. The SceneGraph is ready at this point, so this function can be used to create a
// new manifest or make corrections to an existing one.
AZ::SceneAPI::Events::ProcessingResult LoadingTrackingProcessor::UpdateManifest(AZ::SceneAPI::Containers::Scene& /*scene*/, ManifestAction action,
RequestingApplication /*requester*/)
{
switch (action)
{
case ManifestAction::ConstructDefault:
AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "Constructing a new manifest.");
break;
case ManifestAction::Update:
AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "Updating the manifest.");
break;
default:
AZ_TracePrintf(AZ::SceneAPI::Utilities::WarningWindow, "Unknown manifest update action.");
break;
}
return AZ::SceneAPI::Events::ProcessingResult::Ignored;
}
// With the SceneAPI, the order in which an EBus calls its listeners is mostly random. This generally isn't a problem because most work
// is done in isolation. If there is a dependency, we recommend that you break a call into multiple smaller calls, but this isn't always
// an option. For example, perhaps there is no source code available for third-party extensions or you are trying to avoid making code
// changes to the engine/editor. For those situations, the Call Processor allows you to specify a priority to make sure that a call is made
// before or after all other listeners have done their work.
//
// In this example, we want the log messages to be printed before any other listeners do their work and potentially print their data.
// To accomplish this, we set the priority to the highest available number.
uint8_t LoadingTrackingProcessor::GetPriority() const
{
return EarliestProcessing;
}
// In the constructor, this function was bound to accept any contexts that are derived from ICallContext, which is the base
// for all CallProcessorBus events. This allows for monitoring of everything that happens during the loading process.
AZ::SceneAPI::Events::ProcessingResult LoadingTrackingProcessor::ContextCallback([[maybe_unused]] AZ::SceneAPI::Events::ICallContext& context)
{
AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "LoadEvent: %s", context.RTTI_GetTypeName());
return AZ::SceneAPI::Events::ProcessingResult::Ignored;
}
} // namespace SceneLoggingExample
@@ -0,0 +1,49 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <SceneAPI/SceneCore/Components/LoadingComponent.h>
#include <SceneAPI/SceneCore/Events/AssetImportRequest.h>
namespace SceneLoggingExample
{
// The LoadingTrackingProcessor class demonstrates how to listen to EBus events that start
// and finalize the loading of scene files (such as .fbx files) and the manifest (.assetinfo
// file). It also shows the Call Processor events that can be fired during loading.
class LoadingTrackingProcessor
: public AZ::SceneAPI::SceneCore::LoadingComponent
, public AZ::SceneAPI::Events::AssetImportRequestBus::Handler
{
public:
AZ_COMPONENT(LoadingTrackingProcessor, "{E5E65E21-0BCD-4874-84B8-22E10CCAEE94}", AZ::SceneAPI::SceneCore::LoadingComponent);
LoadingTrackingProcessor();
~LoadingTrackingProcessor() override = default;
void Activate() override;
void Deactivate() override;
static void Reflect(AZ::ReflectContext* context);
AZ::SceneAPI::Events::ProcessingResult PrepareForAssetLoading(AZ::SceneAPI::Containers::Scene& scene,
RequestingApplication requester) override;
AZ::SceneAPI::Events::LoadingResult LoadAsset(AZ::SceneAPI::Containers::Scene& scene,
const AZStd::string& path, const AZ::Uuid& guid, RequestingApplication requester) override;
void FinalizeAssetLoading(AZ::SceneAPI::Containers::Scene& scene, RequestingApplication requester);
AZ::SceneAPI::Events::ProcessingResult UpdateManifest(AZ::SceneAPI::Containers::Scene& scene, ManifestAction action,
RequestingApplication requester) override;
uint8_t GetPriority() const override;
AZ::SceneAPI::Events::ProcessingResult ContextCallback(AZ::SceneAPI::Events::ICallContext& context);
};
} // namespace SceneLoggingExample
@@ -0,0 +1,65 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <IGem.h>
#include <AzCore/Module/DynamicModuleHandle.h>
#include <Behaviors/LoggingGroupBehavior.h>
#include <Processors/LoadingTrackingProcessor.h>
#include <Processors/ExportTrackingProcessor.h>
namespace SceneLoggingExample
{
// The SceneLoggingExampleModule is the entry point for gems. To extend the SceneAPI, the
// logging, loading, and export components must be registered here.
//
// NOTE: The gem system currently does not support registering file extensions through the
// AssetImportRequest EBus.
class SceneLoggingExampleModule
: public CryHooksModule
{
public:
AZ_RTTI(SceneLoggingExampleModule, "{36AA9C0F-7976-40C7-AF54-C492AC5B16F6}", CryHooksModule);
SceneLoggingExampleModule()
: CryHooksModule()
{
// The SceneAPI libraries require specialized initialization. As early as possible, be
// sure to repeat the following two lines for any SceneAPI you want to use. Omitting these
// calls or making them too late can cause problems such as missing EBus events.
m_sceneCoreModule = AZ::DynamicModuleHandle::Create("SceneCore");
m_sceneCoreModule->Load(true);
m_descriptors.insert(m_descriptors.end(),
{
LoggingGroupBehavior::CreateDescriptor(),
LoadingTrackingProcessor::CreateDescriptor(),
ExportTrackingProcessor::CreateDescriptor()
});
}
// In this example, no system components are added. You can use system components
// to set global settings for this gem from the Project Configurator.
// For functionality that should always be available to the SceneAPI, we recommend
// that you use a BehaviorComponent instead.
AZ::ComponentTypeList GetRequiredSystemComponents() const override
{
return AZ::ComponentTypeList {};
}
private:
AZStd::unique_ptr<AZ::DynamicModuleHandle> m_sceneCoreModule;
};
} // namespace SceneLoggingExample
// DO NOT MODIFY THIS LINE UNLESS YOU RENAME THE GEM.
// The first parameter should be GemName_GemIdLower.
// The second should be the fully qualified name of the class above.
AZ_DECLARE_MODULE_CLASS(Gem_SceneLoggingExample, SceneLoggingExample::SceneLoggingExampleModule)
@@ -0,0 +1,14 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include <AzCore/Module/Module.h>
AZ_DECLARE_MODULE_CLASS(Gem_SceneLoggingExample, AZ::Module)
@@ -0,0 +1,22 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(FILES
../ReadMe.txt
Behaviors/LoggingGroupBehavior.h
Behaviors/LoggingGroupBehavior.cpp
Groups/LoggingGroup.h
Groups/LoggingGroup.cpp
Processors/ExportTrackingProcessor.h
Processors/ExportTrackingProcessor.cpp
Processors/LoadingTrackingProcessor.h
Processors/LoadingTrackingProcessor.cpp
)
@@ -0,0 +1,14 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(FILES
SceneLoggingExampleModule.cpp
)
+20
View File
@@ -0,0 +1,20 @@
The Scene Logging Example demonstrates how to extend the SceneAPI by adding additional logging to the pipeline. The SceneAPI is
a collection of libraries that handle loading scene files and converting content to data that the Lumberyard engine and editor can load.
The following approach is used:
1. The FbxSceneBuilder and SceneData load and convert the scene file (for example, .fbx) into a graph that is stored in memory.
2. SceneCore and SceneData are used to create a manifest with instructions about how to export the file.
3. SceneData analyzes the manifest and memory graph and creates defaults.
4. Scene Settings allows updates to the manifest through a UI.
5. The ResourceCompilerScene uses the instructions from the manifest and the data in the graph to create assets. These assets are ready for Lumberyard to use.
The example gem demonstrates the following key features:
- Initialization of the SceneAPI libraries.
(See SceneLoggingExampleModule.cpp)
- Adding a LoadingComponent to hook into the scene loading and react to loading events.
(See Processing/LoadingTrackingProcessor)
- Extension of the Scene Settings UI and ability to set defaults using the BehaviorComponent.
(See Groups/LoggingGroup and Behaviors/LoggingGroupBehavior)
- Adding an ExportingComponent to hook into the scene converting and exporting events.
(See Behaviors/ExportTrackingProcessor)
+11
View File
@@ -0,0 +1,11 @@
{
"GemFormatVersion": 3,
"Uuid": "35d8f6e49ae04c9382c61a42d4355c2f",
"Name": "SceneLoggingExample",
"DisplayName": "Scene Logging Example",
"Version": "0.1.0",
"LinkType": "Dynamic",
"Summary": "This example Gem demonstrates the basics of extending the SceneAPI by adding additional logging to the pipeline. Please read ReadMe.txt for further details.",
"Tags": ["Scene", "FBX"],
"IconPath": "preview.png"
}
+3
View File
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d154a1d97f928c9e6488cdd7f52aef7052551e549d4233fa36ad34c3d8f05fa4
size 39285