You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/SceneLoggingExample/Code/Processors/ExportTrackingProcessor.cpp

190 lines
13 KiB
C++

/*
* 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)
{
// 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.
AZ_TracePrintf(AZ::SceneAPI::Utilities::LogWindow, "'%s' '%s' contains data of type '%s'.", (graph.IsNodeEndPoint(itNodeIndex) ? "End point node" : "Node"),
it->first.GetPath(),
it->second ? it->second->RTTI_GetTypeName() : "No data");
}
}
} // namespace SceneLoggingExample