Merge branch 'main' into Prism/gem-db

main
amzn-mgwynn 5 years ago committed by GitHub
commit ffdce2ef7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,11 +9,6 @@ remove or modify any license notices. This file is distributed on an "AS IS" BAS
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
"""
"""
C24064528: The File menu options function normally
C16780778: The File menu options function normally-New view interaction Model enabled
"""
import os
import sys
@ -54,7 +49,10 @@ class TestFileMenuOptions(EditorTestHelper):
("Save",),
("Save As",),
("Save Level Statistics",),
("Project Settings", "Project Settings Tool"),
("Edit Project Settings",),
("Edit Platform Settings",),
("New Project",),
("Open Project",),
("Show Log File",),
("Resave All Slices",),
("Exit",),

@ -15,6 +15,7 @@ import pytest
# Bail on the test if ly_test_tools doesn't exist.
pytest.importorskip('ly_test_tools')
import ly_test_tools.environment.file_system as file_system
import ly_test_tools._internal.pytest_plugin as internal_plugin
import editor_python_test_tools.hydra_test_utils as hydra
test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts")
@ -40,6 +41,10 @@ class TestBasicEditorWorkflows(object):
@pytest.mark.SUITE_main
def test_BasicEditorWorkflows_LevelEntityComponentCRUD(self, request, editor, level, launcher_platform):
# Skip test if running against Debug build
if "debug" in internal_plugin.build_directory:
pytest.skip("Does not execute against debug builds.")
expected_lines = [
"Create and load new level: True",
"New entity creation: True",

@ -7,8 +7,6 @@ distribution (the "License"). All use of this software is governed by the Licens
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.
C16780783: Base Edit Menu Options (New Viewport Interaction Model)
"""
import os
@ -17,6 +15,7 @@ import pytest
# Bail on the test if ly_test_tools doesn't exist.
pytest.importorskip('ly_test_tools')
import ly_test_tools.environment.file_system as file_system
import ly_test_tools.environment.process_utils as process_utils
import editor_python_test_tools.hydra_test_utils as hydra
test_directory = os.path.join(os.path.dirname(__file__), "EditorScripts")
@ -33,6 +32,7 @@ class TestMenus(object):
def setup_teardown(self, request, workspace, project, level):
def teardown():
file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True)
process_utils.kill_processes_named("o3de", ignore_extensions=True) # Kill ProjectManager windows
request.addfinalizer(teardown)
@ -80,8 +80,7 @@ class TestMenus(object):
expected_lines,
cfg_args=[level],
run_python="--runpython",
auto_test_mode=True,
timeout=log_monitor_timeout,
timeout=log_monitor_timeout
)
@pytest.mark.test_case_id("C16780807")
@ -107,13 +106,13 @@ class TestMenus(object):
"Menus_ViewMenuOptions.py",
expected_lines,
cfg_args=[level],
auto_test_mode=True,
run_python="--runpython",
timeout=log_monitor_timeout,
timeout=log_monitor_timeout
)
@pytest.mark.test_case_id("C16780778")
@pytest.mark.SUITE_sandbox
@pytest.mark.xfail # LYN-4208
def test_Menus_FileMenuOptions_Work(self, request, editor, level, launcher_platform):
expected_lines = [
"New Level Action triggered",
@ -122,7 +121,10 @@ class TestMenus(object):
"Save Action triggered",
"Save As Action triggered",
"Save Level Statistics Action triggered",
"Project Settings Tool Action triggered",
"Edit Project Settings Action triggered",
"Edit Platform Settings Action triggered",
"New Project Action triggered",
"Open Project Action triggered",
"Show Log File Action triggered",
"Resave All Slices Action triggered",
"Exit Action triggered",
@ -135,7 +137,6 @@ class TestMenus(object):
"Menus_FileMenuOptions.py",
expected_lines,
cfg_args=[level],
auto_test_mode=True,
run_python="--runpython",
timeout=log_monitor_timeout,
)
timeout=log_monitor_timeout
)

@ -16,6 +16,7 @@ import logging
# Bail on the test if ly_test_tools doesn't exist.
pytest.importorskip('ly_test_tools')
import ly_test_tools.environment.file_system as file_system
import ly_test_tools._internal.pytest_plugin as internal_plugin
import editor_python_test_tools.hydra_test_utils as hydra
from ly_remote_console.remote_console_commands import RemoteConsole as RemoteConsole
@ -46,6 +47,11 @@ class TestDynamicSliceInstanceSpawner(object):
@pytest.mark.parametrize("launcher_platform", ['windows_editor'])
def test_DynamicSliceInstanceSpawner_DynamicSliceSpawnerWorks(self, request, editor, level, workspace, project,
launcher_platform):
# Skip test if running against Debug build
if "debug" in internal_plugin.build_directory:
pytest.skip("Does not execute against debug builds.")
# Ensure temp level does not already exist
file_system.delete([os.path.join(workspace.paths.engine_root(), project, "Levels", level)], True, True)

@ -16,6 +16,7 @@ import logging
# Bail on the test if ly_test_tools doesn't exist.
pytest.importorskip('ly_test_tools')
import ly_test_tools.environment.file_system as file_system
import ly_test_tools._internal.pytest_plugin as internal_plugin
import editor_python_test_tools.hydra_test_utils as hydra
logger = logging.getLogger(__name__)
@ -40,6 +41,11 @@ class TestEmptyInstanceSpawner(object):
@pytest.mark.SUITE_main
@pytest.mark.dynveg_area
def test_EmptyInstanceSpawner_EmptySpawnerWorks(self, request, editor, level, launcher_platform):
# Skip test if running against Debug build
if "debug" in internal_plugin.build_directory:
pytest.skip("Does not execute against debug builds.")
cfg_args = [level]
expected_lines = [

@ -23,6 +23,7 @@ import pytest
# Bail on the test if ly_test_tools doesn't exist.
pytest.importorskip('ly_test_tools')
import ly_test_tools.environment.file_system as file_system
import ly_test_tools._internal.pytest_plugin as internal_plugin
import editor_python_test_tools.hydra_test_utils as hydra
test_directory = os.path.join(os.path.dirname(__file__), 'EditorScripts')
@ -46,6 +47,11 @@ class TestGraphComponentSync(object):
@pytest.mark.BAT
@pytest.mark.SUITE_main
def test_LandscapeCanvas_SlotConnections_UpdateComponentReferences(self, request, editor, level, launcher_platform):
# Skip test if running against Debug build
if "debug" in internal_plugin.build_directory:
pytest.skip("Does not execute against debug builds.")
cfg_args = [level]
expected_lines = [
@ -122,6 +128,11 @@ class TestGraphComponentSync(object):
"""
Verifies a Gradient Mixer can be setup in Landscape Canvas and all references are property set.
"""
# Skip test if running against Debug build
if "debug" in internal_plugin.build_directory:
pytest.skip("Does not execute against debug builds.")
cfg_args = [level]
expected_lines = [

@ -28,7 +28,13 @@ namespace AZ
{
namespace IdUtils
{
template<typename IdType>
/**
* \param AllowDuplicates - If true allows the same id to be registered multiple times,
with the newer value overwriting the stored value. If false, duplicates are not allowed and
the first stored value is kept.The default is false.
*/
template<typename IdType, bool AllowDuplicates = false>
struct Remapper
{
/**
@ -138,14 +144,18 @@ namespace AZ
* \param context - The serialize context for enumerating the @classPtr elements
*/
template<typename T, typename MapType>
static void GenerateNewIdsAndFixRefs(T* object, MapType& newIdMap, AZ::SerializeContext* context = nullptr)
static void GenerateNewIdsAndFixRefs(
T* object, MapType& newIdMap, AZ::SerializeContext* context = nullptr)
{
if (!context)
{
AZ::ComponentApplicationBus::BroadcastResult(context, &AZ::ComponentApplicationRequests::GetSerializeContext);
if (!context)
{
AZ_Error("Serialization", false, "No serialize context provided! Failed to get component application default serialize context! ComponentApp is not started or input serialize context should not be null!");
AZ_Error(
"Serialization", false,
"No serialize context provided! Failed to get component application default serialize context! ComponentApp is "
"not started or input serialize context should not be null!");
return;
}
}
@ -156,8 +166,16 @@ namespace AZ
{
if (idGenerator)
{
auto it = newIdMap.emplace(originalId, idGenerator());
return it.first->second;
if constexpr(AllowDuplicates)
{
auto it = newIdMap.insert_or_assign(originalId, idGenerator());
return it.first->second;
}
else
{
auto it = newIdMap.emplace(originalId, idGenerator());
return it.first->second;
}
}
return originalId;
}

@ -30,8 +30,10 @@ namespace AZ
bool m_isModifiedContainer;
};
template<typename IdType>
unsigned int Remapper<IdType>::RemapIds(void* classPtr, const AZ::Uuid& classUuid, const typename Remapper<IdType>::IdMapper& mapper, AZ::SerializeContext* context, bool replaceId)
template<typename IdType, bool AllowDuplicates>
unsigned int Remapper<IdType, AllowDuplicates>::RemapIds(
void* classPtr, const AZ::Uuid& classUuid, const typename Remapper<IdType, AllowDuplicates>::IdMapper& mapper,
AZ::SerializeContext* context, bool replaceId)
{
if (!context)
{
@ -152,16 +154,18 @@ namespace AZ
return replaced;
}
template<typename IdType>
unsigned int Remapper<IdType>::ReplaceIdsAndIdRefs(void* classPtr, const AZ::Uuid& classUuid, const IdMapper& mapper, AZ::SerializeContext* context /*= nullptr*/)
template<typename IdType, bool AllowDuplicates>
unsigned int Remapper<IdType, AllowDuplicates>::ReplaceIdsAndIdRefs(void* classPtr, const AZ::Uuid& classUuid, const IdMapper& mapper, AZ::SerializeContext* context /*= nullptr*/)
{
unsigned int replaced = RemapIds(classPtr, classUuid, mapper, context, true);
replaced += RemapIds(classPtr, classUuid, mapper, context, false);
return replaced;
}
template<typename IdType>
unsigned int Remapper<IdType>::RemapIdsAndIdRefs(void* classPtr, const AZ::Uuid& classUuid, const typename Remapper<IdType>::IdReplacer& mapper, AZ::SerializeContext* context)
template<typename IdType, bool AllowDuplicates>
unsigned int Remapper<IdType, AllowDuplicates>::RemapIdsAndIdRefs(
void* classPtr, const AZ::Uuid& classUuid, const typename Remapper<IdType, AllowDuplicates>::IdReplacer& mapper,
AZ::SerializeContext* context)
{
if (!context)
{

@ -14,6 +14,9 @@
#include <sys/syslimits.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <AzCore/std/typetraits/underlying_type.h>
namespace AZ
{

@ -38,20 +38,20 @@ namespace AzFramework
void SpawnableEntitiesContainer::SpawnAllEntities()
{
AZ_Assert(m_threadData, "Calling SpawnAllEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default);
SpawnableEntitiesInterface::Get()->SpawnAllEntities(m_threadData->m_spawnedEntitiesTicket);
}
void SpawnableEntitiesContainer::SpawnEntities(AZStd::vector<size_t> entityIndices)
{
AZ_Assert(m_threadData, "Calling SpawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->SpawnEntities(
m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default, AZStd::move(entityIndices));
m_threadData->m_spawnedEntitiesTicket, AZStd::move(entityIndices));
}
void SpawnableEntitiesContainer::DespawnAllEntities()
{
AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default);
SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_threadData->m_spawnedEntitiesTicket);
}
void SpawnableEntitiesContainer::Reset(AZ::Data::Asset<Spawnable> spawnable)
@ -69,7 +69,6 @@ namespace AzFramework
SpawnableEntitiesInterface::Get()->Barrier(
m_threadData->m_spawnedEntitiesTicket,
SpawnablePriority_Default,
[threadData = m_threadData](EntitySpawnTicket::Id) mutable
{
threadData.reset();
@ -88,7 +87,6 @@ namespace AzFramework
AZ_Assert(m_threadData, "Calling DespawnEntities on a Spawnable container that's not set.");
SpawnableEntitiesInterface::Get()->Barrier(
m_threadData->m_spawnedEntitiesTicket,
SpawnablePriority_Default,
[generation = m_threadData->m_generation, callback = AZStd::move(callback)](EntitySpawnTicket::Id)
{
callback(generation);
@ -115,7 +113,6 @@ namespace AzFramework
AZ_Assert(m_threadData, "SpawnableEntitiesContainer is monitoring a spawnable, but doesn't have the associated data.");
AZ_TracePrintf("Spawnables", "Reloading spawnable '%s'.\n", replacementAsset.GetHint().c_str());
SpawnableEntitiesInterface::Get()->ReloadSpawnable(
m_threadData->m_spawnedEntitiesTicket, SpawnablePriority_Default, AZStd::move(replacementAsset));
SpawnableEntitiesInterface::Get()->ReloadSpawnable(m_threadData->m_spawnedEntitiesTicket, AZStd::move(replacementAsset));
}
} // namespace AzFramework

@ -21,6 +21,7 @@
namespace AZ
{
class Entity;
class SerializeContext;
}
namespace AzFramework
@ -171,6 +172,77 @@ namespace AzFramework
using ClaimEntitiesCallback = AZStd::function<void(EntitySpawnTicket::Id, SpawnableEntityContainerView)>;
using BarrierCallback = AZStd::function<void(EntitySpawnTicket::Id)>;
struct SpawnAllEntitiesOptionalArgs final
{
//! Callback that's called after instances of entities have been created, but before they're spawned into the world. This
//! gives the opportunity to modify the entities if needed such as injecting additional components or modifying components.
EntityPreInsertionCallback m_preInsertionCallback;
//! Callback that's called when spawning entities has completed. This can be triggered from a different thread than the one that
//! made the function call to spawn. The returned list of entities contains all the newly created entities.
EntitySpawnCallback m_completionCallback;
//! The Serialize Context used to clone entities with. If this is not provided the global Serialize Contetx will be used.
AZ::SerializeContext* m_serializeContext { nullptr };
//! The priority at which this call will be executed.
SpawnablePriority m_priority { SpawnablePriority_Default };
};
struct SpawnEntitiesOptionalArgs final
{
//! Callback that's called after instances of entities have been created, but before they're spawned into the world. This
//! gives the opportunity to modify the entities if needed such as injecting additional components or modifying components.
EntityPreInsertionCallback m_preInsertionCallback;
//! Callback that's called when spawning entities has completed. This can be triggered from a different thread than the one that
//! made the function call to spawn. The returned list of entities contains all the newly created entities.
EntitySpawnCallback m_completionCallback;
//! The Serialize Context used to clone entities with. If this is not provided the global Serialize Contetx will be used.
AZ::SerializeContext* m_serializeContext{ nullptr };
//! The priority at which this call will be executed.
SpawnablePriority m_priority{ SpawnablePriority_Default };
//! Entity references are resolved by referring to the last entity spawned from a template entity in the spawnable. If this
//! is set to false entities from previous spawn calls are not taken into account. If set to true entity references may be
//! resolved to a previously spawned entity. A lookup table has to be constructed when true, which may negatively impact
//! performance, especially if a large number of entities are present on a ticket.
bool m_referencePreviouslySpawnedEntities{ false };
};
struct DespawnAllEntitiesOptionalArgs final
{
//! Callback that's called when despawning entities has completed. This can be triggered from a different thread than the one that
//! made the function call to despawn. The returned list of entities contains all the newly created entities.
EntityDespawnCallback m_completionCallback;
//! The priority at which this call will be executed.
SpawnablePriority m_priority { SpawnablePriority_Default };
};
struct ReloadSpawnableOptionalArgs final
{
//! Callback that's called when respawning entities has completed. This can be triggered from a different thread than the one that
//! made the function call to respawn. The returned list of entities contains all the newly created entities.
ReloadSpawnableCallback m_completionCallback;
//! The Serialize Context used to clone entities with. If this is not provided the global Serialize Context will be used.
AZ::SerializeContext* m_serializeContext { nullptr };
//! The priority at which this call will be executed.
SpawnablePriority m_priority { SpawnablePriority_Default };
};
struct ListEntitiesOptionalArgs final
{
//! The priority at which this call will be executed.
SpawnablePriority m_priority{ SpawnablePriority_Default };
};
struct ClaimEntitiesOptionalArgs final
{
//! The priority at which this call will be executed.
SpawnablePriority m_priority{ SpawnablePriority_Default };
};
struct BarrierOptionalArgs final
{
//! The priority at which this call will be executed.
SpawnablePriority m_priority{ SpawnablePriority_Default };
};
//! Interface definition to (de)spawn entities from a spawnable into the game world.
//!
//! While the callbacks of the individual calls are being processed they will block processing any other request. Callbacks can be
@ -196,47 +268,34 @@ namespace AzFramework
//! Spawn instances of all entities in the spawnable.
//! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them.
//! @param priority The priority at which this call will be executed.
//! @param completionCallback Optional callback that's called when spawning entities has completed. This can be called from
//! a different thread than the one that made the function call. The returned list of entities contains all the newly
//! created entities.
virtual void SpawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback = {},
EntitySpawnCallback completionCallback = {}) = 0;
//! @param optionalArgs Optional additional arguments, see SpawnAllEntitiesOptionalArgs.
virtual void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs = {}) = 0;
//! Spawn instances of some entities in the spawnable.
//! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them.
//! @param priority The priority at which this call will be executed.
//! @param entityIndices The indices into the template entities stored in the spawnable that will be used to spawn entities from.
//! @param completionCallback Optional callback that's called when spawning entities has completed. This can be called from
//! a different thread than the one that made this function call. The returned list of entities contains all the newly
//! created entities.
//! @param optionalArgs Optional additional arguments, see SpawnEntitiesOptionalArgs.
virtual void SpawnEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector<size_t> entityIndices,
EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) = 0;
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) = 0;
//! Removes all entities in the provided list from the environment.
//! @param ticket The ticket previously used to spawn entities with.
//! @param priority The priority at which this call will be executed.
//! @param completionCallback Optional callback that's called when despawning entities has completed. This can be called from
//! a different thread than the one that made this function call.
virtual void DespawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback = {}) = 0;
//! @param optionalArgs Optional additional arguments, see DespawnAllEntitiesOptionalArgs.
virtual void DespawnAllEntities(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs = {}) = 0;
//! Removes all entities in the provided list from the environment and reconstructs the entities from the provided spawnable.
//! @param ticket Holds the information on the entities to reload.
//! @param priority The priority at which this call will be executed.
//! @param spawnable The spawnable that will replace the existing spawnable. Both need to have the same asset id.
//! @param completionCallback Optional callback that's called when the entities have been reloaded. This can be called from
//! a different thread than the one that made this function call. The returned list of entities contains all the replacement
//! entities.
//! @param optionalArgs Optional additional arguments, see ReloadSpawnableOptionalArgs.
virtual void ReloadSpawnable(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset<Spawnable> spawnable,
ReloadSpawnableCallback completionCallback = {}) = 0;
EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable, ReloadSpawnableOptionalArgs optionalArgs = {}) = 0;
//! List all entities that are spawned using this ticket.
//! @param ticket Only the entities associated with this ticket will be listed.
//! @param priority The priority at which this call will be executed.
//! @param listCallback Required callback that will be called to list the entities on.
virtual void ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback) = 0;
//! @param optionalArgs Optional additional arguments, see ListEntitiesOptionalArgs.
virtual void ListEntities(
EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs = {}) = 0;
//! List all entities that are spawned using this ticket with their spawnable index.
//! Spawnables contain a flat list of entities, which are used as templates to spawn entities from. For every spawned entity
//! the index of the entity in the spawnable that was used as a template is stored. This version of ListEntities will return
@ -244,23 +303,24 @@ namespace AzFramework
//! the same index may appear multiple times as there are no restriction on how many instance of a specific entity can be
//! created.
//! @param ticket Only the entities associated with this ticket will be listed.
//! @param priority The priority at which this call will be executed.
//! @param listCallback Required callback that will be called to list the entities and indices on.
//! @param optionalArgs Optional additional arguments, see ListEntitiesOptionalArgs.
virtual void ListIndicesAndEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback) = 0;
EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs = {}) = 0;
//! Claim all entities that are spawned using this ticket. Ownership of the entities is transferred from the ticket to the
//! caller through the callback. After this call the ticket will have no entities associated with it. The caller of
//! this function will need to manage the entities after this call.
//! @param ticket Only the entities associated with this ticket will be released.
//! @param priority The priority at which this call will be executed.
//! @param listCallback Required callback that will be called to transfer the entities through.
virtual void ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback) = 0;
//! @param optionalArgs Optional additional arguments, see ClaimEntitiesOptionalArgs.
virtual void ClaimEntities(
EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback, ClaimEntitiesOptionalArgs optionalArgs = {}) = 0;
//! Blocks until all operations made on the provided ticket before the barrier call have completed.
//! @param ticket The ticket to monitor.
//! @param priority The priority at which this call will be executed.
//! @param completionCallback Required callback that will be called as soon as the barrier has been reached.
virtual void Barrier(EntitySpawnTicket& ticket, SpawnablePriority priority, BarrierCallback completionCallback) = 0;
//! @param optionalArgs Optional additional arguments, see BarrierOptionalArgs.
virtual void Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs = {}) = 0;
//! Register a handler for OnSpawned events.
virtual void AddOnSpawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler) = 0;

@ -38,6 +38,10 @@ namespace AzFramework
SpawnableEntitiesManager::SpawnableEntitiesManager()
{
AZ::ComponentApplicationBus::BroadcastResult(m_defaultSerializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
AZ_Assert(
m_defaultSerializeContext, "Failed to retrieve serialization context during construction of the Spawnable Entities Manager.");
if (auto settingsRegistry = AZ::SettingsRegistry::Get(); settingsRegistry != nullptr)
{
AZ::u64 value = aznumeric_caster(m_highPriorityThreshold);
@ -46,58 +50,61 @@ namespace AzFramework
}
}
void SpawnableEntitiesManager::SpawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback,
EntitySpawnCallback completionCallback)
void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnAllEntities hasn't been initialized.");
SpawnAllEntitiesCommand queueEntry;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(completionCallback);
queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback);
QueueRequest(ticket, priority, AZStd::move(queueEntry));
queueEntry.m_serializeContext =
optionalArgs.m_serializeContext == nullptr ? m_defaultSerializeContext : optionalArgs.m_serializeContext;
queueEntry.m_completionCallback = AZStd::move(optionalArgs.m_completionCallback);
queueEntry.m_preInsertionCallback = AZStd::move(optionalArgs.m_preInsertionCallback);
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::SpawnEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector<size_t> entityIndices,
EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback)
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to SpawnEntities hasn't been initialized.");
SpawnEntitiesCommand queueEntry;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_entityIndices = AZStd::move(entityIndices);
queueEntry.m_completionCallback = AZStd::move(completionCallback);
queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback);
QueueRequest(ticket, priority, AZStd::move(queueEntry));
queueEntry.m_serializeContext =
optionalArgs.m_serializeContext == nullptr ? m_defaultSerializeContext : optionalArgs.m_serializeContext;
queueEntry.m_completionCallback = AZStd::move(optionalArgs.m_completionCallback);
queueEntry.m_preInsertionCallback = AZStd::move(optionalArgs.m_preInsertionCallback);
queueEntry.m_referencePreviouslySpawnedEntities = optionalArgs.m_referencePreviouslySpawnedEntities;
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::DespawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback)
void SpawnableEntitiesManager::DespawnAllEntities(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to DespawnAllEntities hasn't been initialized.");
DespawnAllEntitiesCommand queueEntry;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(completionCallback);
QueueRequest(ticket, priority, AZStd::move(queueEntry));
queueEntry.m_completionCallback = AZStd::move(optionalArgs.m_completionCallback);
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ReloadSpawnable(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset<Spawnable> spawnable,
ReloadSpawnableCallback completionCallback)
EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable, ReloadSpawnableOptionalArgs optionalArgs)
{
AZ_Assert(ticket.IsValid(), "Ticket provided to ReloadSpawnable hasn't been initialized.");
ReloadSpawnableCommand queueEntry;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_spawnable = AZStd::move(spawnable);
queueEntry.m_completionCallback = AZStd::move(completionCallback);
QueueRequest(ticket, priority, AZStd::move(queueEntry));
queueEntry.m_serializeContext =
optionalArgs.m_serializeContext == nullptr ? m_defaultSerializeContext : optionalArgs.m_serializeContext;
queueEntry.m_completionCallback = AZStd::move(optionalArgs.m_completionCallback);
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback)
void SpawnableEntitiesManager::ListEntities(
EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs)
{
AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized.");
@ -105,11 +112,11 @@ namespace AzFramework
ListEntitiesCommand queueEntry;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_listCallback = AZStd::move(listCallback);
QueueRequest(ticket, priority, AZStd::move(queueEntry));
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ListIndicesAndEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback)
EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs)
{
AZ_Assert(listCallback, "ListEntities called on spawnable entities without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to ListEntities hasn't been initialized.");
@ -117,10 +124,11 @@ namespace AzFramework
ListIndicesEntitiesCommand queueEntry;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_listCallback = AZStd::move(listCallback);
QueueRequest(ticket, priority, AZStd::move(queueEntry));
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback)
void SpawnableEntitiesManager::ClaimEntities(
EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback, ClaimEntitiesOptionalArgs optionalArgs)
{
AZ_Assert(listCallback, "ClaimEntities called on spawnable entities without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to ClaimEntities hasn't been initialized.");
@ -128,10 +136,10 @@ namespace AzFramework
ClaimEntitiesCommand queueEntry;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_listCallback = AZStd::move(listCallback);
QueueRequest(ticket, priority, AZStd::move(queueEntry));
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::Barrier(EntitySpawnTicket& ticket, SpawnablePriority priority, BarrierCallback completionCallback)
void SpawnableEntitiesManager::Barrier(EntitySpawnTicket& ticket, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs)
{
AZ_Assert(completionCallback, "Barrier on spawnable entities called without a valid callback to use.");
AZ_Assert(ticket.IsValid(), "Ticket provided to Barrier hasn't been initialized.");
@ -139,7 +147,7 @@ namespace AzFramework
BarrierCommand queueEntry;
queueEntry.m_ticketId = ticket.GetId();
queueEntry.m_completionCallback = AZStd::move(completionCallback);
QueueRequest(ticket, priority, AZStd::move(queueEntry));
QueueRequest(ticket, optionalArgs.m_priority, AZStd::move(queueEntry));
}
void SpawnableEntitiesManager::AddOnSpawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler)
@ -174,58 +182,57 @@ namespace AzFramework
auto SpawnableEntitiesManager::ProcessQueue(Queue& queue) -> CommandQueueStatus
{
AZStd::queue<Requests> pendingRequestQueue;
// Process delayed requests first.
// Only process the requests that are currently in this queue, not the ones that could be re-added if they still can't complete.
size_t delayedSize = queue.m_delayed.size();
for (size_t i = 0; i < delayedSize; ++i)
{
AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex);
queue.m_pendingRequest.swap(pendingRequestQueue);
Requests& request = queue.m_delayed.front();
bool result = AZStd::visit(
[this](auto&& args) -> bool
{
return ProcessRequest(args);
},
request);
if (!result)
{
queue.m_delayed.emplace_back(AZStd::move(request));
}
queue.m_delayed.pop_front();
}
if (!pendingRequestQueue.empty() || !queue.m_delayed.empty())
// Process newly added requests.
while (true)
{
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
AZ_Assert(serializeContext, "Failed to retrieve serialization context.");
// Only process the requests that are currently in this queue, not the ones that could be re-added if they still can't complete.
size_t delayedSize = queue.m_delayed.size();
for (size_t i = 0; i < delayedSize; ++i)
AZStd::queue<Requests> pendingRequestQueue;
{
Requests& request = queue.m_delayed.front();
bool result = AZStd::visit([this, serializeContext](auto&& args) -> bool
{
return ProcessRequest(args, *serializeContext);
}, request);
if (!result)
{
queue.m_delayed.emplace_back(AZStd::move(request));
}
queue.m_delayed.pop_front();
AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex);
queue.m_pendingRequest.swap(pendingRequestQueue);
}
do
if (!pendingRequestQueue.empty())
{
while (!pendingRequestQueue.empty())
{
Requests& request = pendingRequestQueue.front();
bool result = AZStd::visit([this, serializeContext](auto&& args) -> bool
bool result = AZStd::visit(
[this](auto&& args) -> bool
{
return ProcessRequest(args, *serializeContext);
}, request);
return ProcessRequest(args);
},
request);
if (!result)
{
queue.m_delayed.emplace_back(AZStd::move(request));
}
pendingRequestQueue.pop();
}
// Spawning entities can result in more entities being queued to spawn. Repeat spawning until the queue is
// empty to avoid a chain of entity spawning getting dragged out over multiple frames.
{
AZStd::scoped_lock queueLock(queue.m_pendingRequestMutex);
queue.m_pendingRequest.swap(pendingRequestQueue);
}
} while (!pendingRequestQueue.empty());
}
}
else
{
break;
}
};
return queue.m_delayed.empty() ? CommandQueueStatus::NoCommandsLeft : CommandQueueStatus::HasCommandsLeft;
}
@ -250,24 +257,14 @@ namespace AzFramework
}
}
AZ::Entity* SpawnableEntitiesManager::SpawnSingleEntity(const AZ::Entity& entityTemplate, AZ::SerializeContext& serializeContext)
{
AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
clone->SetId(AZ::Entity::MakeId());
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, clone);
return clone;
}
AZ::Entity* SpawnableEntitiesManager::CloneSingleEntity(const AZ::Entity& entityTemplate,
EntityIdMap& templateToCloneEntityIdMap, AZ::SerializeContext& serializeContext)
EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext)
{
return AZ::IdUtils::Remapper<AZ::EntityId>::CloneObjectAndGenerateNewIdsAndFixRefs(
&entityTemplate, templateToCloneEntityIdMap, &serializeContext);
return AZ::IdUtils::Remapper<AZ::EntityId, true>::CloneObjectAndGenerateNewIdsAndFixRefs(
&entityTemplate, templateToCloneMap, &serializeContext);
}
bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext)
bool SpawnableEntitiesManager::ProcessRequest(SpawnAllEntitiesCommand& request)
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
@ -291,13 +288,9 @@ namespace AzFramework
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
// Mark all indices as spawned
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
{
const AZ::Entity& entityTemplate = *entitiesToSpawn[i];
AZ::Entity* clone = CloneSingleEntity(entityTemplate, templateToCloneEntityIdMap, serializeContext);
AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[i], templateToCloneEntityIdMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
spawnedEntities.emplace_back(clone);
@ -305,16 +298,8 @@ namespace AzFramework
}
// loadAll is true if every entity has been spawned only once
if (spawnedEntities.size() == entitiesToSpawnSize)
{
ticket.m_loadAll = true;
}
else
{
// Case where there were already spawns from a previous request
ticket.m_loadAll = false;
}
ticket.m_loadAll = (spawnedEntities.size() == entitiesToSpawnSize);
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
if (request.m_preInsertionCallback)
{
@ -323,11 +308,10 @@ namespace AzFramework
}
// Add to the game context, now the entities are active
AZStd::for_each(ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end(),
[](AZ::Entity* entity)
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
{
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, entity);
});
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
// Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context.
if (request.m_completionCallback)
@ -347,21 +331,41 @@ namespace AzFramework
}
}
bool SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request, AZ::SerializeContext& serializeContext)
bool SpawnableEntitiesManager::ProcessRequest(SpawnEntitiesCommand& request)
{
Ticket& ticket = *request.m_ticket;
if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId)
{
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
AZ_Assert(
spawnedEntities.size() == spawnedEntityIndices.size(),
"The indices for the spawned entities has gone out of sync with the entities.");
// Keep track how many entities there were in the array initially
// Keep track of how many entities there were in the array initially
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
// These are 'template' entities we'll be cloning from
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
size_t entitiesToSpawnSize = request.m_entityIndices.size();
// Reconstruct the template to entity mapping.
EntityIdMap templateToCloneEntityIdMap;
if (!request.m_referencePreviouslySpawnedEntities)
{
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
}
else
{
templateToCloneEntityIdMap.reserve(spawnedEntitiesInitialCount + entitiesToSpawnSize);
SpawnableConstIndexEntityContainerView indexEntityView(
spawnedEntities.begin(), spawnedEntityIndices.begin(), spawnedEntities.size());
for (auto& entry : indexEntityView)
{
templateToCloneEntityIdMap.insert_or_assign(entitiesToSpawn[entry.GetIndex()]->GetId(), entry.GetEntity()->GetId());
}
}
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
@ -369,15 +373,11 @@ namespace AzFramework
{
if (index < entitiesToSpawn.size())
{
const AZ::Entity& entityTemplate = *entitiesToSpawn[index];
AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate);
AZ::Entity* clone = CloneSingleEntity(*entitiesToSpawn[index], templateToCloneEntityIdMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
clone->SetId(AZ::Entity::MakeId());
spawnedEntities.push_back(clone);
spawnedEntityIndices.push_back(index);
}
}
ticket.m_loadAll = false;
@ -390,11 +390,10 @@ namespace AzFramework
}
// Add to the game context, now the entities are active
AZStd::for_each(ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end(),
[](AZ::Entity* entity)
for (auto it = ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount; it != ticket.m_spawnedEntities.end(); ++it)
{
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, entity);
});
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, *it);
}
if (request.m_completionCallback)
{
@ -413,8 +412,7 @@ namespace AzFramework
}
}
bool SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request,
[[maybe_unused]] AZ::SerializeContext& serializeContext)
bool SpawnableEntitiesManager::ProcessRequest(DespawnAllEntitiesCommand& request)
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@ -447,7 +445,7 @@ namespace AzFramework
}
}
bool SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request, AZ::SerializeContext& serializeContext)
bool SpawnableEntitiesManager::ProcessRequest(ReloadSpawnableCommand& request)
{
Ticket& ticket = *request.m_ticket;
AZ_Assert(ticket.m_spawnable.GetId() == request.m_spawnable.GetId(),
@ -470,39 +468,43 @@ namespace AzFramework
// Rebuild the list of entities.
ticket.m_spawnedEntities.clear();
const Spawnable::EntityList& entities = request.m_spawnable->GetEntities();
// Map keeps track of ids from template (spawnable) to clone (instance)
// Allowing patch ups of fields referring to entityIds outside of a given entity
EntityIdMap templateToCloneEntityIdMap;
if (ticket.m_loadAll)
{
// The new spawnable may have a different number of entities and since the intent of the user was
// to load every, simply start over.
// to spawn every entity, simply start over.
ticket.m_spawnedEntityIndices.clear();
size_t entitiesToSpawnSize = entities.size();
// Map keeps track of ids from template (spawnable) to clone (instance)
// Allowing patch ups of fields referring to entityIds outside of a given entity
EntityIdMap templateToCloneEntityIdMap;
templateToCloneEntityIdMap.reserve(entitiesToSpawnSize);
// Mark all indices as spawned
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
{
const AZ::Entity& entityTemplate = *entities[i];
AZ::Entity* clone = CloneSingleEntity(entityTemplate, templateToCloneEntityIdMap, serializeContext);
AZ::Entity* clone = CloneSingleEntity(*entities[i], templateToCloneEntityIdMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
ticket.m_spawnedEntities.emplace_back(clone);
ticket.m_spawnedEntities.push_back(clone);
ticket.m_spawnedEntityIndices.push_back(i);
}
}
else
{
size_t entitiesSize = entities.size();
templateToCloneEntityIdMap.reserve(entitiesSize);
for (size_t index : ticket.m_spawnedEntityIndices)
{
ticket.m_spawnedEntities.push_back(
index < entitiesSize ? SpawnSingleEntity(*entities[index], serializeContext) : nullptr);
// It's possible for the new spawnable to have a different number of entities, so guard against this.
// It's also possible that the entities have moved within the spawnable to a new index. This can't be
// detected and will result in the incorrect entities being spawned.
if (index < entitiesSize)
{
AZ::Entity* clone = CloneSingleEntity(*entities[index], templateToCloneEntityIdMap, *request.m_serializeContext);
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
ticket.m_spawnedEntities.push_back(clone);
}
}
}
ticket.m_spawnable = AZStd::move(request.m_spawnable);
@ -525,7 +527,7 @@ namespace AzFramework
}
}
bool SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
bool SpawnableEntitiesManager::ProcessRequest(ListEntitiesCommand& request)
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@ -541,7 +543,7 @@ namespace AzFramework
}
}
bool SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
bool SpawnableEntitiesManager::ProcessRequest(ListIndicesEntitiesCommand& request)
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@ -560,7 +562,7 @@ namespace AzFramework
}
}
bool SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
bool SpawnableEntitiesManager::ProcessRequest(ClaimEntitiesCommand& request)
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@ -580,7 +582,7 @@ namespace AzFramework
}
}
bool SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
bool SpawnableEntitiesManager::ProcessRequest(BarrierCommand& request)
{
Ticket& ticket = *request.m_ticket;
if (request.m_requestId == ticket.m_currentRequestId)
@ -599,7 +601,7 @@ namespace AzFramework
}
}
bool SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request, [[maybe_unused]] AZ::SerializeContext& serializeContext)
bool SpawnableEntitiesManager::ProcessRequest(DestroyTicketCommand& request)
{
if (request.m_requestId == request.m_ticket->m_currentRequestId)
{

@ -37,7 +37,7 @@ namespace AzFramework
AZ_CLASS_ALLOCATOR(SpawnableEntitiesManager, AZ::SystemAllocator, 0);
using EntityIdMap = AZStd::unordered_map<AZ::EntityId, AZ::EntityId>;
enum class CommandQueueStatus : bool
{
HasCommandsLeft,
@ -57,26 +57,21 @@ namespace AzFramework
// The following functions are thread safe
//
void SpawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityPreInsertionCallback preInsertionCallback = {},
EntitySpawnCallback completionCallback = {}) override;
void SpawnAllEntities(EntitySpawnTicket& ticket, SpawnAllEntitiesOptionalArgs optionalArgs = {}) override;
void SpawnEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZStd::vector<size_t> entityIndices,
EntityPreInsertionCallback preInsertionCallback = {},
EntitySpawnCallback completionCallback = {}) override;
void DespawnAllEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, EntityDespawnCallback completionCallback = {}) override;
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, SpawnEntitiesOptionalArgs optionalArgs = {}) override;
void DespawnAllEntities(EntitySpawnTicket& ticket, DespawnAllEntitiesOptionalArgs optionalArgs = {}) override;
void ReloadSpawnable(
EntitySpawnTicket& ticket, SpawnablePriority priority, AZ::Data::Asset<Spawnable> spawnable,
ReloadSpawnableCallback completionCallback = {}) override;
EntitySpawnTicket& ticket, AZ::Data::Asset<Spawnable> spawnable, ReloadSpawnableOptionalArgs optionalArgs = {}) override;
void ListEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ListEntitiesCallback listCallback) override;
void ListEntities(
EntitySpawnTicket& ticket, ListEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs = {}) override;
void ListIndicesAndEntities(
EntitySpawnTicket& ticket, SpawnablePriority priority, ListIndicesEntitiesCallback listCallback) override;
void ClaimEntities(EntitySpawnTicket& ticket, SpawnablePriority priority, ClaimEntitiesCallback listCallback) override;
EntitySpawnTicket& ticket, ListIndicesEntitiesCallback listCallback, ListEntitiesOptionalArgs optionalArgs = {}) override;
void ClaimEntities(
EntitySpawnTicket& ticket, ClaimEntitiesCallback listCallback, ClaimEntitiesOptionalArgs optionalArgs = {}) override;
void Barrier(EntitySpawnTicket& spawnInfo, SpawnablePriority priority, BarrierCallback completionCallback) override;
void Barrier(EntitySpawnTicket& spawnInfo, BarrierCallback completionCallback, BarrierOptionalArgs optionalArgs = {}) override;
void AddOnSpawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler) override;
void AddOnDespawnedHandler(AZ::Event<AZ::Data::Asset<Spawnable>>::Handler& handler) override;
@ -105,6 +100,7 @@ namespace AzFramework
{
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
AZ::SerializeContext* m_serializeContext;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
@ -114,9 +110,11 @@ namespace AzFramework
AZStd::vector<size_t> m_entityIndices;
EntitySpawnCallback m_completionCallback;
EntityPreInsertionCallback m_preInsertionCallback;
AZ::SerializeContext* m_serializeContext;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
bool m_referencePreviouslySpawnedEntities;
};
struct DespawnAllEntitiesCommand
{
@ -129,6 +127,7 @@ namespace AzFramework
{
AZ::Data::Asset<Spawnable> m_spawnable;
ReloadSpawnableCallback m_completionCallback;
AZ::SerializeContext* m_serializeContext;
Ticket* m_ticket;
EntitySpawnTicket::Id m_ticketId;
uint32_t m_requestId;
@ -185,21 +184,18 @@ namespace AzFramework
CommandQueueStatus ProcessQueue(Queue& queue);
AZ::Entity* SpawnSingleEntity(const AZ::Entity& entityTemplate,
AZ::SerializeContext& serializeContext);
AZ::Entity* CloneSingleEntity(const AZ::Entity& entityTemplate,
EntityIdMap& templateToCloneEntityIdMap, AZ::SerializeContext& serializeContext);
bool ProcessRequest(SpawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(SpawnEntitiesCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(DespawnAllEntitiesCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(ReloadSpawnableCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(ListEntitiesCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(ListIndicesEntitiesCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(ClaimEntitiesCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(BarrierCommand& request, AZ::SerializeContext& serializeContext);
bool ProcessRequest(DestroyTicketCommand& request, AZ::SerializeContext& serializeContext);
AZ::Entity* CloneSingleEntity(
const AZ::Entity& entityTemplate, EntityIdMap& templateToCloneMap, AZ::SerializeContext& serializeContext);
bool ProcessRequest(SpawnAllEntitiesCommand& request);
bool ProcessRequest(SpawnEntitiesCommand& request);
bool ProcessRequest(DespawnAllEntitiesCommand& request);
bool ProcessRequest(ReloadSpawnableCommand& request);
bool ProcessRequest(ListEntitiesCommand& request);
bool ProcessRequest(ListIndicesEntitiesCommand& request);
bool ProcessRequest(ClaimEntitiesCommand& request);
bool ProcessRequest(BarrierCommand& request);
bool ProcessRequest(DestroyTicketCommand& request);
Queue m_highPriorityQueue;
Queue m_regularPriorityQueue;
@ -207,6 +203,7 @@ namespace AzFramework
AZ::Event<AZ::Data::Asset<Spawnable>> m_onSpawnedEvent;
AZ::Event<AZ::Data::Asset<Spawnable>> m_onDespawnedEvent;
AZ::SerializeContext* m_defaultSerializeContext { nullptr };
//! The threshold used to determine if a request goes in the regular (if bigger than the value) or high priority queue (if smaller
//! or equal to this value). The starting value of 64 is chosen as it's between default values SpawnablePriority_High and
//! SpawnablePriority_Default which gives users a bit of room to fine tune the priorities as this value can be configured

@ -128,12 +128,12 @@ namespace AzFramework
worldPosition, CameraView(cameraState), CameraProjection(cameraState), cameraState.m_viewportSize);
}
AZ::Vector3 ScreenToWorld(
const ScreenPoint& screenPosition, const AZ::Matrix4x4& inverseCameraView,
const AZ::Matrix4x4& inverseCameraProjection, const AZ::Vector2& viewportSize)
AZ::Vector3 ScreenNDCToWorld(
const AZ::Vector2& normalizedScreenPosition, const AZ::Matrix4x4& inverseCameraView,
const AZ::Matrix4x4& inverseCameraProjection)
{
// convert screen space coordinates from <0, 1> to <-1,1> range
const auto ndcPosition = NDCFromScreenPoint(screenPosition, viewportSize) * 2.0f - AZ::Vector2::CreateOne();
const auto ndcPosition = normalizedScreenPosition * 2.0f - AZ::Vector2::CreateOne();
// transform ndc space position to clip space
const auto clipSpacePosition = inverseCameraProjection * Vector2ToVector4(ndcPosition, -1.0f, 1.0f);
@ -145,6 +145,15 @@ namespace AzFramework
return worldPosition;
}
AZ::Vector3 ScreenToWorld(
const ScreenPoint& screenPosition, const AZ::Matrix4x4& inverseCameraView,
const AZ::Matrix4x4& inverseCameraProjection, const AZ::Vector2& viewportSize)
{
const auto normalizedScreenPosition = NDCFromScreenPoint(screenPosition, viewportSize);
return ScreenNDCToWorld(normalizedScreenPosition, inverseCameraView, inverseCameraProjection);
}
AZ::Vector3 ScreenToWorld(const ScreenPoint& screenPosition, const CameraState& cameraState)
{
return ScreenToWorld(

@ -42,7 +42,7 @@ namespace AzFramework
const AZ::Vector3& worldPosition, const AZ::Matrix4x4& cameraView, const AZ::Matrix4x4& cameraProjection,
const AZ::Vector2& viewportSize);
//! Unprojects a position in screen space to world space.
//! Unprojects a position in screen space pixel coordinates to world space.
//! Note: The position returned will be on the near clip plane of the camera in world space.
AZ::Vector3 ScreenToWorld(const ScreenPoint& screenPosition, const CameraState& cameraState);
@ -52,6 +52,12 @@ namespace AzFramework
const ScreenPoint& screenPosition, const AZ::Matrix4x4& inverseCameraView,
const AZ::Matrix4x4& inverseCameraProjection, const AZ::Vector2& viewportSize);
//! Unprojects a position in screen space normalized device coordinates to world space.
//! Note: The position returned will be on the near clip plane of the camera in world space.
AZ::Vector3 ScreenNDCToWorld(
const AZ::Vector2& ndcPosition, const AZ::Matrix4x4& inverseCameraView,
const AZ::Matrix4x4& inverseCameraProjection);
//! Returns the camera projection for the current camera state.
AZ::Matrix4x4 CameraProjection(const CameraState& cameraState);

@ -139,7 +139,7 @@ namespace AzNetworking
NetworkOutputSerializer networkSerializer(buffer.GetBuffer(), buffer.GetSize());
{
ISerializer& serializer = networkSerializer; // To get the default typeinfo parameters in ISerializer
ISerializer& networkISerializer = networkSerializer; // To get the default typeinfo parameters in ISerializer
// First, serialize out the header
if (!header.SerializePacketFlags(networkSerializer))
@ -148,7 +148,7 @@ namespace AzNetworking
return false;
}
if (!serializer.Serialize(header, "Header"))
if (!networkISerializer.Serialize(header, "Header"))
{
AZLOG(NET_FragmentQueue, "Reconstructed fragmented packet failed header serialization");
return false;

@ -14,6 +14,7 @@
#include <AzCore/Math/Matrix3x3.h>
#include <AzCore/Math/Matrix4x4.h>
#include <AzCore/Math/Transform.h>
#include <AzCore/Math/VectorConversions.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/Viewport/CameraState.h>
#include <AzFramework/Viewport/ViewportScreen.h>
@ -23,6 +24,15 @@
namespace UnitTest
{
// transform a point from normalized device coordinates to world space, and then from world space back to normalized device coordinates
AZ::Vector2 ScreenNDCToWorldToScreenNDC(
const AZ::Vector2& ndcPoint, const AzFramework::CameraState& cameraState)
{
const auto worldResult = AzFramework::ScreenNDCToWorld(ndcPoint, InverseCameraView(cameraState), InverseCameraProjection(cameraState));
const auto ndcResult = AzFramework::WorldToScreenNDC(worldResult, CameraView(cameraState), CameraProjection(cameraState));
return AZ::Vector3ToVector2(ndcResult);
}
// transform a point from screen space to world space, and then from world space back to screen space
AzFramework::ScreenPoint ScreenToWorldToScreen(
const AzFramework::ScreenPoint& screenPoint, const AzFramework::CameraState& cameraState)
@ -30,7 +40,8 @@ namespace UnitTest
const auto worldResult = AzFramework::ScreenToWorld(screenPoint, cameraState);
return AzFramework::WorldToScreen(worldResult, cameraState);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
// ScreenPoint tests
TEST(ViewportScreen, WorldToScreenAndScreenToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin)
{
using AzFramework::ScreenPoint;
@ -38,8 +49,6 @@ namespace UnitTest
const auto screenDimensions = AZ::Vector2(800.0f, 600.0f);
const auto cameraPosition = AZ::Vector3::CreateAxisY(-10.0f);
// note: nearClip is 0.1 - the world space value returned will be aligned to the near clip
// plane of the camera so use that to confirm the mapping to/from is correct
const auto cameraState = AzFramework::CreateIdentityDefaultCamera(cameraPosition, screenDimensions);
{
const auto expectedScreenPoint = ScreenPoint{600, 450};
@ -81,6 +90,8 @@ namespace UnitTest
EXPECT_EQ(resultScreenPoint, expectedScreenPoint);
}
// note: nearClip is 0.1 - the world space value returned will be aligned to the near clip
// plane of the camera so use that to confirm the mapping to/from is correct
TEST(ViewportScreen, ScreenToWorldReturnsPositionOnNearClipPlaneInWorldSpace)
{
using AzFramework::ScreenPoint;
@ -94,7 +105,75 @@ namespace UnitTest
const auto worldResult = AzFramework::ScreenToWorld(ScreenPoint{400, 300}, cameraState);
EXPECT_THAT(worldResult, IsClose(AZ::Vector3(10.1f, 0.0f, 0.0f)));
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
// NDC tests
TEST(ViewportScreen, WorldToScreenNDCAndScreenNDCToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin)
{
using NdcPoint = AZ::Vector2;
const auto screenDimensions = AZ::Vector2(800.0f, 600.0f);
const auto cameraPosition = AZ::Vector3::CreateAxisY(-10.0f);
const auto cameraState = AzFramework::CreateIdentityDefaultCamera(cameraPosition, screenDimensions);
{
const auto expectedNdcPoint = NdcPoint{0.75f, 0.75f};
const auto resultNdcPoint = ScreenNDCToWorldToScreenNDC(expectedNdcPoint, cameraState);
EXPECT_THAT(resultNdcPoint, IsClose(expectedNdcPoint));
}
{
const auto expectedNdcPoint = NdcPoint{0.5f, 0.5f};
const auto resultNdcPoint = ScreenNDCToWorldToScreenNDC(expectedNdcPoint, cameraState);
EXPECT_THAT(resultNdcPoint, IsClose(expectedNdcPoint));
}
{
const auto expectedNdcPoint = NdcPoint{0.0f, 0.0f};
const auto resultNdcPoint = ScreenNDCToWorldToScreenNDC(expectedNdcPoint, cameraState);
EXPECT_THAT(resultNdcPoint, IsClose(expectedNdcPoint));
}
{
const auto expectedNdcPoint = NdcPoint{1.0f, 1.0f};
const auto resultNdcPoint = ScreenNDCToWorldToScreenNDC(expectedNdcPoint, cameraState);
EXPECT_THAT(resultNdcPoint, IsClose(expectedNdcPoint));
}
}
TEST(ViewportScreen, WorldToScreenNDCAndScreenNDCToWorldReturnsTheSameValueOrientatedCamera)
{
using NdcPoint = AZ::Vector2;
const auto screenDimensions = AZ::Vector2(800.0f, 600.0f);
const auto cameraTransform =
AZ::Transform::CreateRotationX(AZ::DegToRad(45.0f)) * AZ::Transform::CreateRotationZ(AZ::DegToRad(90.0f));
const auto cameraState = AzFramework::CreateDefaultCamera(cameraTransform, screenDimensions);
const auto expectedNdcPoint = NdcPoint{0.25f, 0.5f};
const auto resultNdcPoint = ScreenNDCToWorldToScreenNDC(expectedNdcPoint, cameraState);
EXPECT_THAT(resultNdcPoint, IsClose(expectedNdcPoint));
}
// note: nearClip is 0.1 - the world space value returned will be aligned to the near clip
// plane of the camera so use that to confirm the mapping to/from is correct
TEST(ViewportScreen, ScreenNDCToWorldReturnsPositionOnNearClipPlaneInWorldSpace)
{
using NdcPoint = AZ::Vector2;
const auto screenDimensions = AZ::Vector2(800.0f, 600.0f);
const auto cameraTransform = AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 0.0f, 0.0f)) *
AZ::Transform::CreateRotationZ(AZ::DegToRad(-90.0f));
const auto cameraState = AzFramework::CreateDefaultCamera(cameraTransform, screenDimensions);
const auto worldResult = AzFramework::ScreenNDCToWorld(NdcPoint{0.5f, 0.5f}, InverseCameraView(cameraState), InverseCameraProjection(cameraState));
EXPECT_THAT(worldResult, IsClose(AZ::Vector3(10.1f, 0.0f, 0.0f)));
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
// ScreenVector tests
TEST(ViewportScreen, SubstractingScreenPointGivesScreenVector)
{
using AzFramework::ScreenPoint;
@ -220,6 +299,8 @@ namespace UnitTest
EXPECT_NEAR(AzFramework::ScreenVectorLength(ScreenVector(12, 15)), 19.20937f, 0.001f);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
// Other tests
TEST(ViewportScreen, CanGetCameraTransformFromCameraViewAndBack)
{
const auto screenDimensions = AZ::Vector2(1024.0f, 768.0f);

@ -11,9 +11,11 @@
*/
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/UserSettings/UserSettingsComponent.h>
#include <AzFramework/Application/Application.h>
#include <AzFramework/Spawnable/SpawnableAssetHandler.h>
#include <AzFramework/Spawnable/SpawnableEntitiesManager.h>
#include <AzFramework/Components/TransformComponent.h>
#include <AzTest/AzTest.h>
namespace UnitTest
@ -40,6 +42,10 @@ namespace UnitTest
m_application = new TestApplication();
AZ::ComponentApplication::Descriptor descriptor;
m_application->Start(descriptor);
// Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
// shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash
// in the unit tests.
AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize);
m_spawnable = aznew AzFramework::Spawnable(
AZ::Data::AssetId::CreateString("{EB2E8A2B-F253-4A90-BBF4-55F2EED786B8}:0"), AZ::Data::AssetData::AssetStatus::Ready);
@ -81,6 +87,42 @@ namespace UnitTest
}
}
void CreateRecursiveHierarchy()
{
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
size_t numElements = entities.size();
AZ::EntityId parent;
for (size_t i=0; i<numElements; ++i)
{
AZStd::unique_ptr<AZ::Entity>& entity = entities[i];
auto component = entity->CreateComponent<AzFramework::TransformComponent>();
if (i > 0)
{
component->SetParent(parent);
}
parent = entity->GetId();
}
}
void CreateSingleParent()
{
AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities();
size_t numElements = entities.size();
if (numElements > 0)
{
AZ::EntityId parent = entities[0]->GetId();
for (size_t i = 0; i < numElements; ++i)
{
AZStd::unique_ptr<AZ::Entity>& entity = entities[i];
auto component = entity->CreateComponent<AzFramework::TransformComponent>();
if (i > 0)
{
component->SetParent(parent);
}
}
}
}
protected:
AZ::Data::Asset<AzFramework::Spawnable>* m_spawnableAsset { nullptr };
AzFramework::SpawnableEntitiesManager* m_manager { nullptr };
@ -104,17 +146,50 @@ namespace UnitTest
{
spawnedEntitiesCount += entities.size();
};
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default, {}, AZStd::move(callback));
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(NumEntities, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_SetParentOnSpawnedEntities_LineageIsPreserved)
{
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
CreateRecursiveHierarchy();
auto callback = [](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
AZ::EntityId parentId;
bool isFirst = true;
for (const AZ::Entity* entity : entities)
{
if (!isFirst)
{
auto transform = entity->GetTransform();
ASSERT_NE(nullptr, transform);
EXPECT_EQ(parentId, transform->GetParentId());
}
else
{
isFirst = false;
}
parentId = entity->GetId();
}
};
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash)
{
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->SpawnAllEntities(ticket, AzFramework::SpawnablePriority_Default);
m_manager->SpawnAllEntities(ticket);
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
@ -124,11 +199,175 @@ namespace UnitTest
// SpawnEntities
//
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_Call_AllEntitiesSpawned)
{
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZStd::vector<size_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(NumEntities, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_SpawnTheSameEntity_AllEntitiesSpawned)
{
static constexpr size_t NumEntities = 1;
FillSpawnable(NumEntities);
AZStd::vector<size_t> indices = { 0, 0 };
size_t spawnedEntitiesCount = 0;
auto callback =
[&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(NumEntities * 2, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_MultipleSpawns_AllEntitiesSpawned)
{
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
AZStd::vector<size_t> indices = { 0, 2, 3, 1 };
size_t spawnedEntitiesCount = 0;
auto callback =
[&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
spawnedEntitiesCount += entities.size();
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
m_manager->SpawnEntities(*m_ticket, indices, optionalArgs);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_EQ(NumEntities * 2, spawnedEntitiesCount);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_ReferencesAreRemappedForNewBatch_AllPointToLatestParent)
{
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
CreateSingleParent();
AZStd::vector<size_t> indices = { 0, 1, 2, 3 };
AZStd::vector<AZ::EntityId> parents;
auto callback = [&parents](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
AZ::EntityId parent = (*entities.begin())->GetId();
parents.push_back(parent);
auto it = entities.begin();
++it; // Skip the first as that is the parent.
for (; it != entities.end(); ++it)
{
AZ::TransformInterface* transform = (*it)->GetTransform();
ASSERT_NE(nullptr, transform);
ASSERT_EQ(parent, transform->GetParentId());
}
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
optionalArgs.m_referencePreviouslySpawnedEntities = false;
m_manager->SpawnEntities(*m_ticket, indices, optionalArgs);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_NE(parents[0], parents[1]);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_ReferencesAreRemappedForContinuedBatch_AllPointToLatestParent)
{
static constexpr size_t NumEntities = 4;
FillSpawnable(NumEntities);
CreateSingleParent();
AZStd::vector<size_t> indices = { 0, 1, 2, 3 };
AZStd::vector<AZ::EntityId> parents;
auto callback =
[&parents](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
AZ::EntityId parent = (*entities.begin())->GetId();
parents.push_back(parent);
auto it = entities.begin();
++it; // Skip the first as that is the parent.
for (; it!=entities.end(); ++it)
{
AZ::TransformInterface* transform = (*it)->GetTransform();
ASSERT_NE(nullptr, transform);
ASSERT_EQ(parent, transform->GetParentId());
}
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(callback);
optionalArgs.m_referencePreviouslySpawnedEntities = true;
m_manager->SpawnEntities(*m_ticket, indices, optionalArgs);
m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_NE(parents[0], parents[1]);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_ReferencesAreRemappedAcrossBatches_AllPointToLatestParent)
{
FillSpawnable(4);
CreateSingleParent();
// Spawn a regular batch but with two parents and store the id of the last entity. This will the parent for the next batch.
AZ::EntityId parent;
auto getParent = [&parent](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
ASSERT_NE(entities.begin(), entities.end());
parent = (*AZStd::prev(entities.end()))->GetId();
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgsFirstBatch;
optionalArgsFirstBatch.m_completionCallback = AZStd::move(getParent);
optionalArgsFirstBatch.m_referencePreviouslySpawnedEntities = true;
m_manager->SpawnEntities(*m_ticket, {0, 1, 2, 3, 0}, AZStd::move(optionalArgsFirstBatch));
// Next, spawn all the entities that have a reference to the parent that was just stored.
auto parentCheck = [&parent](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities)
{
for (auto& it : entities)
{
AZ::TransformInterface* transform = it->GetTransform();
ASSERT_NE(nullptr, transform);
ASSERT_EQ(parent, transform->GetParentId());
}
};
AzFramework::SpawnEntitiesOptionalArgs optionalArgsSecondBatch;
optionalArgsSecondBatch.m_completionCallback = AZStd::move(parentCheck);
optionalArgsSecondBatch.m_referencePreviouslySpawnedEntities = true;
m_manager->SpawnEntities(*m_ticket, {1, 2, 3}, AZStd::move(optionalArgsSecondBatch));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_DeleteTicketBeforeCall_NoCrash)
{
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->SpawnEntities(ticket, AzFramework::SpawnablePriority_Default, {});
m_manager->SpawnEntities(ticket, {/* Deliberate empty list of indices. */});
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
@ -142,7 +381,7 @@ namespace UnitTest
{
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->DespawnAllEntities(ticket, AzFramework::SpawnablePriority_Default);
m_manager->DespawnAllEntities(ticket);
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
@ -156,7 +395,7 @@ namespace UnitTest
{
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->ReloadSpawnable(ticket, AzFramework::SpawnablePriority_Default, *m_spawnableAsset);
m_manager->ReloadSpawnable(ticket, *m_spawnableAsset);
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
@ -183,8 +422,8 @@ namespace UnitTest
spawnedEntitiesCount += entities.size();
};
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default);
m_manager->ListEntities(*m_ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
m_manager->SpawnAllEntities(*m_ticket);
m_manager->ListEntities(*m_ticket, AZStd::move(callback));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_TRUE(allValidEntityIds);
@ -197,7 +436,7 @@ namespace UnitTest
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->ListEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
m_manager->ListEntities(ticket, AZStd::move(callback));
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
@ -228,8 +467,8 @@ namespace UnitTest
}
};
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default);
m_manager->ListIndicesAndEntities(*m_ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
m_manager->SpawnAllEntities(*m_ticket);
m_manager->ListIndicesAndEntities(*m_ticket, AZStd::move(callback));
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
EXPECT_TRUE(allValidEntityIds);
@ -242,7 +481,7 @@ namespace UnitTest
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->ListIndicesAndEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
m_manager->ListIndicesAndEntities(ticket, AZStd::move(callback));
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
@ -258,7 +497,7 @@ namespace UnitTest
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->ClaimEntities(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
m_manager->ClaimEntities(ticket, AZStd::move(callback));
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
@ -274,7 +513,7 @@ namespace UnitTest
{
AzFramework::EntitySpawnTicket ticket(*m_spawnableAsset);
m_manager->Barrier(ticket, AzFramework::SpawnablePriority_Default, AZStd::move(callback));
m_manager->Barrier(ticket, AZStd::move(callback));
}
m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
}
@ -305,8 +544,16 @@ namespace UnitTest
defaultPriorityCallId = callCounter++;
};
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default, {}, AZStd::move(defaultCallback));
m_manager->SpawnAllEntities(highPriorityTicket, AzFramework::SpawnablePriority_High, {}, AZStd::move(highCallback));
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(defaultCallback);
optionalArgs.m_priority = AzFramework::SpawnablePriority_Default;
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
AzFramework::SpawnAllEntitiesOptionalArgs highPriortyOptionalArgs;
highPriortyOptionalArgs.m_completionCallback = AZStd::move(highCallback);
highPriortyOptionalArgs.m_priority = AzFramework::SpawnablePriority_High;
m_manager->SpawnAllEntities(highPriorityTicket, AZStd::move(highPriortyOptionalArgs));
m_manager->ProcessQueue(
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High |
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);
@ -333,8 +580,16 @@ namespace UnitTest
defaultPriorityCallId = callCounter++;
};
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_Default, {}, AZStd::move(defaultCallback));
m_manager->SpawnAllEntities(*m_ticket, AzFramework::SpawnablePriority_High, {}, AZStd::move(highCallback));
AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs;
optionalArgs.m_completionCallback = AZStd::move(defaultCallback);
optionalArgs.m_priority = AzFramework::SpawnablePriority_Default;
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs));
AzFramework::SpawnAllEntitiesOptionalArgs highPriortyOptionalArgs;
highPriortyOptionalArgs.m_completionCallback = AZStd::move(highCallback);
highPriortyOptionalArgs.m_priority = AzFramework::SpawnablePriority_High;
m_manager->SpawnAllEntities(*m_ticket, AZStd::move(highPriortyOptionalArgs));
m_manager->ProcessQueue(
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::High |
AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular);

@ -191,8 +191,8 @@ ly_add_translations(
)
ly_add_dependencies(Editor AssetProcessor)
if(LY_FIRST_PROJECT_PATH)
set_property(TARGET Editor APPEND PROPERTY VS_DEBUGGER_COMMAND_ARGUMENTS "--project-path=\"${LY_FIRST_PROJECT_PATH}\"")
if(LY_DEFAULT_PROJECT_PATH)
set_property(TARGET Editor APPEND PROPERTY VS_DEBUGGER_COMMAND_ARGUMENTS "--project-path=\"${LY_DEFAULT_PROJECT_PATH}\"")
endif()
################################################################################

@ -118,10 +118,10 @@ void CVarMenu::AddUniqueCVarsItem(QString displayName,
// Otherwise we could have just used the action's currently checked
// state and updated the CVar's value only
bool cVarOn = (cVar->GetFVal() == availableCVar.m_onValue);
bool checked = !cVarOn;
SetCVar(cVar, checked ? availableCVar.m_onValue : availableCVar.m_offValue);
action->setChecked(checked);
if (checked)
bool cVarChecked = !cVarOn;
SetCVar(cVar, cVarChecked ? availableCVar.m_onValue : availableCVar.m_offValue);
action->setChecked(cVarChecked);
if (cVarChecked)
{
// Set the rest of the CVars in the group to their off values
SetCVarsToOffValue(availableCVars, availableCVar);
@ -132,9 +132,9 @@ void CVarMenu::AddUniqueCVarsItem(QString displayName,
// Initialize the action's checked state based on its associated CVar's current value
ICVar* cVar = gEnv->pConsole->GetCVar(availableCVar.m_cVarName.toUtf8().data());
bool checked = (cVar && cVar->GetFVal() == availableCVar.m_onValue);
action->setChecked(checked);
if (checked)
bool cVarChecked = (cVar && cVar->GetFVal() == availableCVar.m_onValue);
action->setChecked(cVarChecked);
if (cVarChecked)
{
// Set the rest of the CVars in the group to their off values
SetCVarsToOffValue(availableCVars, availableCVar);

@ -205,10 +205,10 @@ CTrackViewAnimNode::CTrackViewAnimNode(IAnimSequence* pSequence, IAnimNode* anim
for (int i = 0; i < nodeCount; ++i)
{
IAnimNode* node = pSequence->GetNode(i);
IAnimNode* pParentNode = node->GetParent();
IAnimNode* pNodeParentNode = node->GetParent();
// If our node is the parent, then the current node is a child of it
if (animNode == pParentNode)
if (animNode == pNodeParentNode)
{
CTrackViewAnimNodeFactory animNodeFactory;
CTrackViewAnimNode* pNewTVAnimNode = animNodeFactory.BuildAnimNode(pSequence, node, this);

@ -68,12 +68,12 @@ CTrackViewTrack::CTrackViewTrack(IAnimTrack* pTrack, CTrackViewAnimNode* pTrackA
{
// Search for child tracks
const unsigned int subTrackCount = m_pAnimTrack->GetSubTrackCount();
for (unsigned int subTrackIndex = 0; subTrackIndex < subTrackCount; ++subTrackIndex)
for (unsigned int subTrackI = 0; subTrackI < subTrackCount; ++subTrackI)
{
IAnimTrack* pSubTrack = m_pAnimTrack->GetSubTrack(subTrackIndex);
IAnimTrack* pSubTrack = m_pAnimTrack->GetSubTrack(subTrackI);
CTrackViewTrackFactory trackFactory;
CTrackViewTrack* pNewTVTrack = trackFactory.BuildTrack(pSubTrack, pTrackAnimNode, this, true, subTrackIndex);
CTrackViewTrack* pNewTVTrack = trackFactory.BuildTrack(pSubTrack, pTrackAnimNode, this, true, subTrackI);
m_childNodes.push_back(std::unique_ptr<CTrackViewNode>(pNewTVTrack));
}

@ -125,8 +125,8 @@ ly_add_target(
AZ::AssetProcessorBatch.Static
)
if(LY_FIRST_PROJECT_PATH)
set_property(TARGET AssetProcessor AssetProcessorBatch APPEND PROPERTY VS_DEBUGGER_COMMAND_ARGUMENTS "--project-path=\"${LY_FIRST_PROJECT_PATH}\"")
if(LY_DEFAULT_PROJECT_PATH)
set_property(TARGET AssetProcessor AssetProcessorBatch APPEND PROPERTY VS_DEBUGGER_COMMAND_ARGUMENTS "--project-path=\"${LY_DEFAULT_PROJECT_PATH}\"")
endif()
# Adds the AssetProcessorBatch target as a C preprocessor define so that it can be used as a Settings Registry

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.02554 10L9 15.0362V18L0 9L9 0V2.98885L4 8H18V10H4.02554Z" fill="#1e70eb"/>
</svg>

After

Width:  |  Height:  |  Size: 230 B

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 17L15 14H9L12 17Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 190 B

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f82f22df64b93d4bec91e56b60efa3d5ce2915ce388a2dc627f1ab720678e3d5
size 334987
oid sha256:4a5881b8d6cfbc4ceefb14ab96844484fe19407ee030824768f9fcce2f729d35
size 2949

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8358f4dad9878c662b9819b2b346622af691eb45f8eddc28fff79a50650ae6cf
size 2503

@ -7,6 +7,7 @@
<file>AddOffset.svg</file>
<file>AddOffset_Hover.svg</file>
<file>ArrowBack.svg</file>
<file>ArrowBack_Hover.svg</file>
<file>build.svg</file>
<file>FolderOffset.svg</file>
<file>FolderOffset_Hover.svg</file>
@ -18,6 +19,7 @@
<file>Linux.svg</file>
<file>macOS.svg</file>
<file>DefaultProjectImage.png</file>
<file>DefaultTemplate.png</file>
<file>ArrowDownLine.svg</file>
<file>ArrowUpLine.svg</file>
<file>o3de.svg</file>
@ -26,5 +28,8 @@
<file>Backgrounds/FirstTimeBackgroundImage.jpg</file>
<file>ArrowDownLine.svg</file>
<file>ArrowUpLine.svg</file>
<file>CarrotArrowDown.svg</file>
<file>Summary.svg</file>
<file>WindowClose.svg</file>
</qresource>
</RCC>

@ -115,7 +115,7 @@ QTabBar::tab:pressed
/************** General (Modal windows) **************/
#header {
background-color:#111111;
background-color:#111111;
min-height:80px;
max-height:80px;
}
@ -136,11 +136,9 @@ QTabBar::tab:pressed
#header QPushButton:focus {
border:none;
}
#header QPushButton:hover {
background:#333333 url(:/ArrowBack.svg) no-repeat center;
}
#header QPushButton:hover,
#header QPushButton:pressed {
background:#222222 url(:/ArrowBack.svg) no-repeat center;
background:transparent url(:/ArrowBack_Hover.svg) no-repeat center;
}
#headerTitle {
@ -172,8 +170,8 @@ QTabBar::tab:pressed
#footer > QPushButton {
qproperty-flat: true;
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #0095f2, stop: 1.0 #1e70eb);
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #0095f2, stop: 1.0 #1e70eb);
border-radius: 3px;
min-height: 28px;
max-height: 28px;
@ -181,26 +179,26 @@ QTabBar::tab:pressed
margin-right:30px;
}
#footer > QPushButton:hover {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #10A5f2, stop: 1.0 #2e80eb);
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #10A5f2, stop: 1.0 #2e80eb);
}
#footer > QPushButton:pressed {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #0085e2, stop: 1.0 #0e60db);
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #0085e2, stop: 1.0 #0e60db);
}
#footer > QPushButton[secondary="true"] {
margin-right: 10px;
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #888888, stop: 1.0 #555555);
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #888888, stop: 1.0 #555555);
}
#footer > QPushButton[secondary="true"]:hover {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #999999, stop: 1.0 #666666);
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #999999, stop: 1.0 #666666);
}
#footer > QPushButton[secondary="true"]:pressed {
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #555555, stop: 1.0 #777777);
background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
stop: 0 #555555, stop: 1.0 #777777);
}
/************** Project Settings **************/
@ -210,9 +208,6 @@ QTabBar::tab:pressed
#projectTemplate {
margin: 55px 0 0 50px;
max-width: 780px;
min-height:200px;
max-height:200px;
}
#projectTemplateLabel {
font-size:16px;
@ -227,11 +222,67 @@ QTabBar::tab:pressed
#projectTemplateDetails {
background-color:#444444;
max-width:240px;
max-width:20%;
min-width:240px;
margin-left:30px;
}
#projectTemplateDetails #displayName,
#projectTemplateDetails #includedGemsTitle {
font-size:18px;
}
#projectTemplateDetails #moreGems {
font-size:14px;
margin-top:20px;
}
#projectTemplateDetails #includedGemsTitle {
margin-top:5px;
margin-bottom:5px;
}
#projectTemplateDetails #summary {
padding-bottom:0px;
border-bottom:2px solid #555555;
min-height:80px;
qproperty-alignment: AlignTop;
}
#projectTemplateDetails #browseCatalog {
margin:5px 0px 15px 0px;
}
#projectTemplate QPushButton {
qproperty-flat: true;
min-width: 96px;
max-width: 96px;
min-height: 160px;
max-height: 160px;
}
#projectTemplate #templateLabel {
qproperty-alignment: AlignCenter;
}
#projectTemplate QPushButton #templateImage {
border:3px solid transparent;
border-radius: 4px;
}
#projectTemplate QPushButton[Checked="true"] #templateImage {
border:3px solid #1e70eb;
}
#projectTemplate QPushButton[Checked="true"] #templateLabel {
font-weight:bold;
}
#projectTemplate QPushButton:hover {
background-color: #444444;
}
#projectTemplate QPushButton:focus {
outline: none;
border:none;
}
#projectSettingsTab::tab-bar {
left: 60px;
}
@ -311,6 +362,7 @@ QTabBar::tab:pressed
#projectButton > #labelButton {
border:1px solid white;
}
#projectButton > #labelButton:hover,
#projectButton > #labelButton:pressed {
border:1px solid #1e70eb;
@ -350,12 +402,38 @@ QTabBar::tab:pressed
max-height:278px;
}
QProgressBar {
border: none;
background-color: transparent;
padding: 0px;
min-height: 14px;
font-size: 2px;
}
QProgressBar::chunk {
background-color: #1E70EB;
}
/************** Gem Catalog **************/
#GemCatalogTitle {
font-size: 18px;
}
#GemCatalogCart {
background-color: #555555;
}
#GemCatalogCartCountLabel {
font-size: 12px;
background-color: #4285F4;
border-radius: 3px;
}
#GemCatalogCartOverlaySectionLabel {
font-weight: 600;
}
/************** Gem Catalog (Inspector) **************/
#GemCatalogInspector {

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.5714 6.7619H16.7619C16.7619 4.13333 14.6286 2 12 2C9.37145 2 7.23811 4.13333 7.23811 6.7619H3.42859V22H20.5714V6.7619ZM12 3.90476C13.581 3.90476 14.8572 5.18095 14.8572 6.7619H9.14287C9.14287 5.18095 10.4191 3.90476 12 3.90476ZM13.7997 11.7424L12.0288 9.24095H11.9712L10.2003 11.7424H13.7997ZM5.00002 12.4335L10.9158 19.4063L9.03253 12.4335H5.00002ZM10.14 12.4335L11.9356 19.5147C11.9548 19.5189 11.974 19.521 11.9959 19.521C12.0178 19.521 12.0398 19.5189 12.0617 19.5147L13.86 12.4335H10.14ZM14.9675 12.4335H18.8547H19L13.0842 19.398L14.9675 12.4335ZM8.26457 9.24095H11.075L9.52787 11.4253L8.26457 9.24095ZM8.83789 11.7424L7.5193 9.46191L5.01921 11.7424H5.14804H8.83789ZM18.9808 11.7424H18.8547H15.1512L16.4286 9.41397L18.9808 11.7424ZM15.6558 9.24095H12.9252L14.4647 11.4146L15.6558 9.24095Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 966 B

@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5507 19.5506L19.5213 2.58008L21.289 4.34785L4.31846 21.3184L2.5507 19.5506Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.28706 2.65015L21.2576 19.6207L19.4899 21.3885L2.51929 4.41791L4.28706 2.65015Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 394 B

@ -15,6 +15,7 @@
#include <PythonBindingsInterface.h>
#include <NewProjectSettingsScreen.h>
#include <ScreenHeaderWidget.h>
#include <GemCatalog/GemCatalogScreen.h>
#include <QDialogButtonBox>
#include <QHBoxLayout>
@ -41,24 +42,33 @@ namespace O3DE::ProjectManager
m_stack = new QStackedWidget(this);
m_stack->setObjectName("body");
m_stack->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding));
m_stack->addWidget(new NewProjectSettingsScreen());
m_gemCatalog = new GemCatalogScreen();
m_stack->addWidget(m_gemCatalog);
m_stack->setSizePolicy(QSizePolicy(QSizePolicy::Preferred,QSizePolicy::Expanding));
m_newProjectSettingsScreen = new NewProjectSettingsScreen(this);
m_stack->addWidget(m_newProjectSettingsScreen);
m_gemCatalogScreen = new GemCatalogScreen(this);
m_stack->addWidget(m_gemCatalogScreen);
vLayout->addWidget(m_stack);
QDialogButtonBox* backNextButtons = new QDialogButtonBox();
backNextButtons->setObjectName("footer");
vLayout->addWidget(backNextButtons);
QDialogButtonBox* buttons = new QDialogButtonBox();
buttons->setObjectName("footer");
vLayout->addWidget(buttons);
m_backButton = backNextButtons->addButton(tr("Back"), QDialogButtonBox::RejectRole);
m_backButton->setProperty("secondary", true);
m_nextButton = backNextButtons->addButton(tr("Next"), QDialogButtonBox::ApplyRole);
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
connect(m_newProjectSettingsScreen, &ScreenWidget::ChangeScreenRequest, this, &CreateProjectCtrl::OnChangeScreenRequest);
connect(m_backButton, &QPushButton::clicked, this, &CreateProjectCtrl::HandleBackButton);
connect(m_nextButton, &QPushButton::clicked, this, &CreateProjectCtrl::HandleNextButton);
m_secondaryButton = buttons->addButton(tr("Back"), QDialogButtonBox::RejectRole);
m_secondaryButton->setProperty("secondary", true);
m_secondaryButton->setVisible(false);
connect(m_secondaryButton, &QPushButton::clicked, this, &CreateProjectCtrl::HandleSecondaryButton);
Update();
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
m_primaryButton = buttons->addButton(tr("Create Project"), QDialogButtonBox::ApplyRole);
connect(m_primaryButton, &QPushButton::clicked, this, &CreateProjectCtrl::HandlePrimaryButton);
setLayout(vLayout);
}
@ -80,8 +90,10 @@ namespace O3DE::ProjectManager
{
if (m_stack->currentIndex() > 0)
{
m_stack->setCurrentIndex(m_stack->currentIndex() - 1);
Update();
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
PreviousScreen();
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
}
else
{
@ -89,70 +101,123 @@ namespace O3DE::ProjectManager
}
}
void CreateProjectCtrl::HandleNextButton()
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
void CreateProjectCtrl::HandleSecondaryButton()
{
ScreenWidget* currentScreen = reinterpret_cast<ScreenWidget*>(m_stack->currentWidget());
ProjectManagerScreen screenEnum = currentScreen->GetScreenEnum();
if (m_stack->currentIndex() > 0)
{
// return to Project Settings page
PreviousScreen();
}
else
{
// Configure Gems
NextScreen();
}
}
void CreateProjectCtrl::Update()
{
if (m_stack->currentWidget() == m_gemCatalogScreen)
{
m_header->setSubTitle(tr("Configure project with Gems"));
m_secondaryButton->setVisible(false);
}
else
{
m_header->setSubTitle(tr("Enter Project Details"));
m_secondaryButton->setVisible(true);
m_secondaryButton->setText(tr("Configure Gems"));
}
}
if (screenEnum == ProjectManagerScreen::NewProjectSettings)
void CreateProjectCtrl::OnChangeScreenRequest(ProjectManagerScreen screen)
{
if (screen == ProjectManagerScreen::GemCatalog)
{
HandleSecondaryButton();
}
else
{
auto newProjectScreen = reinterpret_cast<NewProjectSettingsScreen*>(currentScreen);
if (newProjectScreen)
emit ChangeScreenRequest(screen);
}
}
void CreateProjectCtrl::NextScreen()
{
if (m_stack->currentIndex() < m_stack->count())
{
if(CurrentScreenIsValid())
{
if (!newProjectScreen->Validate())
{
QMessageBox::critical(this, tr("Invalid project settings"), tr("Invalid project settings"));
return;
}
m_stack->setCurrentIndex(m_stack->currentIndex() + 1);
m_projectInfo = newProjectScreen->GetProjectInfo();
m_projectTemplatePath = newProjectScreen->GetProjectTemplatePath();
QString projectTemplatePath = m_newProjectSettingsScreen->GetProjectTemplatePath();
m_gemCatalogScreen->ReinitForProject(projectTemplatePath + "/Template", /*isNewProject=*/true);
// The next page is the gem catalog. Gather the available gems that will be shown in the gem catalog.
m_gemCatalog->ReinitForProject(m_projectInfo.m_path, /*isNewProject=*/true);
Update();
}
else
{
QMessageBox::warning(this, tr("Invalid project settings"), tr("Please correct the indicated project settings and try again."));
}
}
}
if (m_stack->currentIndex() != m_stack->count() - 1)
void CreateProjectCtrl::PreviousScreen()
{
// we don't require the current screen to be valid when moving back
if (m_stack->currentIndex() > 0)
{
m_stack->setCurrentIndex(m_stack->currentIndex() + 1);
m_stack->setCurrentIndex(m_stack->currentIndex() - 1);
Update();
}
else
}
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
void CreateProjectCtrl::HandlePrimaryButton()
{
CreateProject();
}
bool CreateProjectCtrl::CurrentScreenIsValid()
{
if (m_stack->currentWidget() == m_newProjectSettingsScreen)
{
auto result = PythonBindingsInterface::Get()->CreateProject(m_projectTemplatePath, m_projectInfo);
return m_newProjectSettingsScreen->Validate();
}
return true;
}
void CreateProjectCtrl::CreateProject()
{
if (m_newProjectSettingsScreen->Validate())
{
ProjectInfo projectInfo = m_newProjectSettingsScreen->GetProjectInfo();
QString projectTemplatePath = m_newProjectSettingsScreen->GetProjectTemplatePath();
auto result = PythonBindingsInterface::Get()->CreateProject(projectTemplatePath, projectInfo);
if (result.IsSuccess())
{
// automatically register the project
PythonBindingsInterface::Get()->AddProject(m_projectInfo.m_path);
PythonBindingsInterface::Get()->AddProject(projectInfo.m_path);
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
m_gemCatalogScreen->EnableDisableGemsForProject(projectInfo.m_path);
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
// adding gems is not implemented yet because we don't know what targets to add or how to add them
projectInfo.m_needsBuild = true;
emit NotifyBuildProject(projectInfo);
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
else
{
QMessageBox::critical(this, tr("Project creation failed"), tr("Failed to create project."));
}
// Enable/disable gems for the newly created project.
m_gemCatalog->EnableDisableGemsForProject(m_projectInfo.m_path);
}
}
void CreateProjectCtrl::Update()
{
ScreenWidget* currentScreen = reinterpret_cast<ScreenWidget*>(m_stack->currentWidget());
if (currentScreen && currentScreen->GetScreenEnum() == ProjectManagerScreen::GemCatalog)
{
m_header->setTitle(tr("Create Project"));
m_header->setSubTitle(tr("Configure project with Gems"));
m_nextButton->setText(tr("Create Project"));
}
else
{
m_header->setTitle(tr("Create Project"));
m_header->setSubTitle(tr("Enter Project Details"));
m_nextButton->setText(tr("Next"));
QMessageBox::warning(this, tr("Invalid project settings"), tr("Please correct the indicated project settings and try again."));
}
}

@ -14,9 +14,11 @@
#if !defined(Q_MOC_RUN)
#include <ScreenWidget.h>
#include <ProjectInfo.h>
#include <GemCatalog/GemCatalogScreen.h>
#endif
// due to current limitations, customizing template Gems is disabled
#define TEMPLATE_GEM_CONFIGURATION_ENABLED
QT_FORWARD_DECLARE_CLASS(QStackedWidget)
QT_FORWARD_DECLARE_CLASS(QPushButton)
QT_FORWARD_DECLARE_CLASS(QLabel)
@ -24,6 +26,8 @@ QT_FORWARD_DECLARE_CLASS(QLabel)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(ScreenHeader)
QT_FORWARD_DECLARE_CLASS(NewProjectSettingsScreen)
QT_FORWARD_DECLARE_CLASS(GemCatalogScreen)
class CreateProjectCtrl
: public ScreenWidget
@ -36,21 +40,37 @@ namespace O3DE::ProjectManager
protected slots:
void HandleBackButton();
void HandleNextButton();
void HandlePrimaryButton();
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
void OnChangeScreenRequest(ProjectManagerScreen screen);
void HandleSecondaryButton();
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
private:
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
void Update();
void NextScreen();
void PreviousScreen();
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
bool CurrentScreenIsValid();
void CreateProject();
QStackedWidget* m_stack;
ScreenHeader* m_header;
QStackedWidget* m_stack = nullptr;
ScreenHeader* m_header = nullptr;
QPushButton* m_backButton;
QPushButton* m_nextButton;
QPushButton* m_primaryButton = nullptr;
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
QPushButton* m_secondaryButton = nullptr;
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
QString m_projectTemplatePath;
ProjectInfo m_projectInfo;
GemCatalogScreen* m_gemCatalog = nullptr;
NewProjectSettingsScreen* m_newProjectSettingsScreen = nullptr;
GemCatalogScreen* m_gemCatalogScreen = nullptr;
};
} // namespace O3DE::ProjectManager

@ -10,24 +10,229 @@
*
*/
#include <AzQtComponents/Components/SearchLineEdit.h>
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QLabel>
#include <QPushButton>
#include <TagWidget.h>
namespace O3DE::ProjectManager
{
GemCatalogHeaderWidget::GemCatalogHeaderWidget(GemSortFilterProxyModel* filterProxyModel, QWidget* parent)
CartOverlayWidget::CartOverlayWidget(GemModel* gemModel, QWidget* parent)
: QWidget(parent)
, m_gemModel(gemModel)
{
setObjectName("GemCatalogCart");
m_layout = new QVBoxLayout();
m_layout->setSpacing(0);
m_layout->setMargin(0);
m_layout->setAlignment(Qt::AlignTop);
setLayout(m_layout);
QHBoxLayout* hLayout = new QHBoxLayout();
QPushButton* closeButton = new QPushButton();
closeButton->setFlat(true);
closeButton->setFocusPolicy(Qt::NoFocus);
closeButton->setIcon(QIcon(":/WindowClose.svg"));
connect(closeButton, &QPushButton::clicked, this, [=]
{
deleteLater();
});
hLayout->addSpacerItem(new QSpacerItem(10, 0, QSizePolicy::Expanding));
hLayout->addWidget(closeButton);
m_layout->addLayout(hLayout);
// enabled
{
m_enabledWidget = new QWidget();
m_enabledWidget->setFixedWidth(s_width);
m_layout->addWidget(m_enabledWidget);
QVBoxLayout* layout = new QVBoxLayout();
layout->setAlignment(Qt::AlignTop);
m_enabledWidget->setLayout(layout);
m_enabledLabel = new QLabel();
m_enabledLabel->setObjectName("GemCatalogCartOverlaySectionLabel");
layout->addWidget(m_enabledLabel);
m_enabledTagContainer = new TagContainerWidget();
layout->addWidget(m_enabledTagContainer);
}
// disabled
{
m_disabledWidget = new QWidget();
m_disabledWidget->setFixedWidth(s_width);
m_layout->addWidget(m_disabledWidget);
QVBoxLayout* layout = new QVBoxLayout();
layout->setAlignment(Qt::AlignTop);
m_disabledWidget->setLayout(layout);
m_disabledLabel = new QLabel();
m_disabledLabel->setObjectName("GemCatalogCartOverlaySectionLabel");
layout->addWidget(m_disabledLabel);
m_disabledTagContainer = new TagContainerWidget();
layout->addWidget(m_disabledTagContainer);
}
setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
Update();
connect(gemModel, &GemModel::dataChanged, this, [=]
{
Update();
});
}
void CartOverlayWidget::Update()
{
const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded();
if (toBeAdded.isEmpty())
{
m_enabledWidget->hide();
}
else
{
m_enabledTagContainer->Update(ConvertFromModelIndices(toBeAdded));
m_enabledLabel->setText(QString("%1 %2").arg(QString::number(toBeAdded.size()), tr("Gems to be enabled")));
m_enabledWidget->show();
}
const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved();
if (toBeRemoved.isEmpty())
{
m_disabledWidget->hide();
}
else
{
m_disabledTagContainer->Update(ConvertFromModelIndices(toBeRemoved));
m_disabledLabel->setText(QString("%1 %2").arg(QString::number(toBeRemoved.size()), tr("Gems to be disabled")));
m_disabledWidget->show();
}
}
QStringList CartOverlayWidget::ConvertFromModelIndices(const QVector<QModelIndex>& gems) const
{
QStringList gemNames;
gemNames.reserve(gems.size());
for (const QModelIndex& modelIndex : gems)
{
gemNames.push_back(GemModel::GetName(modelIndex));
}
return gemNames;
}
CartButton::CartButton(GemModel* gemModel, QWidget* parent)
: QWidget(parent)
, m_gemModel(gemModel)
{
m_layout = new QHBoxLayout();
m_layout->setMargin(0);
setLayout(m_layout);
QPushButton* iconButton = new QPushButton();
iconButton->setFlat(true);
iconButton->setFocusPolicy(Qt::NoFocus);
iconButton->setIcon(QIcon(":/Summary.svg"));
iconButton->setFixedSize(s_iconSize, s_iconSize);
connect(iconButton, &QPushButton::clicked, this, &CartButton::ShowOverlay);
m_layout->addWidget(iconButton);
m_countLabel = new QLabel();
m_countLabel->setObjectName("GemCatalogCartCountLabel");
m_countLabel->setFixedHeight(s_iconSize - 1); // Compensate for the empty icon space by using a slightly smaller label height.
m_layout->addWidget(m_countLabel);
m_dropDownButton = new QPushButton();
m_dropDownButton->setFlat(true);
m_dropDownButton->setFocusPolicy(Qt::NoFocus);
m_dropDownButton->setIcon(QIcon(":/CarrotArrowDown.svg"));
m_dropDownButton->setFixedSize(s_arrowDownIconSize, s_arrowDownIconSize);
connect(m_dropDownButton, &QPushButton::clicked, this, &CartButton::ShowOverlay);
m_layout->addWidget(m_dropDownButton);
// Adjust the label text whenever the model gets updated.
connect(gemModel, &GemModel::dataChanged, [=]
{
const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded();
const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved();
const int count = toBeAdded.size() + toBeRemoved.size();
m_countLabel->setText(QString::number(count));
m_dropDownButton->setVisible(!toBeAdded.isEmpty() || !toBeRemoved.isEmpty());
// Automatically close the overlay window in case there are no gems to be enabled or disabled anymore.
if (m_cartOverlay && toBeAdded.isEmpty() && toBeRemoved.isEmpty())
{
m_cartOverlay->deleteLater();
m_cartOverlay = nullptr;
}
});
}
void CartButton::mousePressEvent([[maybe_unused]] QMouseEvent* event)
{
ShowOverlay();
}
void CartButton::ShowOverlay()
{
const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded();
const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved();
if (toBeAdded.isEmpty() && toBeRemoved.isEmpty())
{
return;
}
if (m_cartOverlay)
{
// Directly delete the former overlay before creating the new one.
// Don't use deleteLater() here. This might overwrite the new overlay pointer
// depending on the event queue.
delete m_cartOverlay;
}
m_cartOverlay = new CartOverlayWidget(m_gemModel, this);
connect(m_cartOverlay, &QWidget::destroyed, this, [=]
{
// Reset the overlay pointer on destruction to prevent dangling pointers.
m_cartOverlay = nullptr;
});
m_cartOverlay->show();
const QPoint parentPos = m_dropDownButton->mapToParent(m_dropDownButton->pos());
const QPoint globalPos = m_dropDownButton->mapToGlobal(m_dropDownButton->pos());
const QPoint offset(-4, 10);
m_cartOverlay->setGeometry(globalPos.x() - parentPos.x() - m_cartOverlay->width() + width() + offset.x(),
globalPos.y() + offset.y(),
m_cartOverlay->width(),
m_cartOverlay->height());
}
CartButton::~CartButton()
{
// Make sure the overlay window is automatically closed in case the gem catalog is destroyed.
if (m_cartOverlay)
{
m_cartOverlay->deleteLater();
}
}
GemCatalogHeaderWidget::GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, QWidget* parent)
: QFrame(parent)
{
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setAlignment(Qt::AlignLeft);
hLayout->setMargin(0);
hLayout->setContentsMargins(10, 7, 10, 7);
setLayout(hLayout);
setObjectName("GemCatalogHeaderWidget");
hLayout->addSpacing(7);
setFixedHeight(s_height);
QLabel* titleLabel = new QLabel(tr("Gem Catalog"));
titleLabel->setObjectName("GemCatalogTitle");
@ -35,17 +240,23 @@ namespace O3DE::ProjectManager
hLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding));
AzQtComponents::SearchLineEdit* filterLineEdit = new AzQtComponents::SearchLineEdit();
filterLineEdit->setStyleSheet("background-color: #DDDDDD;");
connect(filterLineEdit, &QLineEdit::textChanged, this, [=](const QString& text)
m_filterLineEdit = new AzQtComponents::SearchLineEdit();
m_filterLineEdit->setStyleSheet("background-color: #DDDDDD;");
connect(m_filterLineEdit, &QLineEdit::textChanged, this, [=](const QString& text)
{
filterProxyModel->SetSearchString(text);
});
hLayout->addWidget(filterLineEdit);
hLayout->addWidget(m_filterLineEdit);
hLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding));
hLayout->addSpacerItem(new QSpacerItem(140, 0, QSizePolicy::Fixed));
setFixedHeight(60);
hLayout->addSpacerItem(new QSpacerItem(75, 0, QSizePolicy::Fixed));
CartButton* cartButton = new CartButton(gemModel);
hLayout->addWidget(cartButton);
}
void GemCatalogHeaderWidget::ReinitForProject()
{
m_filterLineEdit->setText({});
}
} // namespace O3DE::ProjectManager

@ -13,19 +13,81 @@
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzQtComponents/Components/SearchLineEdit.h>
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <TagWidget.h>
#include <QFrame>
#include <QLabel>
#include <QDialog>
#include <QMoveEvent>
#include <QVBoxLayout>
#endif
namespace O3DE::ProjectManager
{
class CartOverlayWidget
: public QWidget
{
Q_OBJECT // AUTOMOC
public:
CartOverlayWidget(GemModel* gemModel, QWidget* parent = nullptr);
void Update();
private:
QStringList ConvertFromModelIndices(const QVector<QModelIndex>& gems) const;
QVBoxLayout* m_layout = nullptr;
GemModel* m_gemModel = nullptr;
QWidget* m_enabledWidget = nullptr;
QLabel* m_enabledLabel = nullptr;
TagContainerWidget* m_enabledTagContainer = nullptr;
QWidget* m_disabledWidget = nullptr;
QLabel* m_disabledLabel = nullptr;
TagContainerWidget* m_disabledTagContainer = nullptr;
inline constexpr static int s_width = 240;
};
class CartButton
: public QWidget
{
Q_OBJECT // AUTOMOC
public:
CartButton(GemModel* gemModel, QWidget* parent = nullptr);
~CartButton();
void ShowOverlay();
private:
void mousePressEvent(QMouseEvent* event) override;
GemModel* m_gemModel = nullptr;
QHBoxLayout* m_layout = nullptr;
QLabel* m_countLabel = nullptr;
QPushButton* m_dropDownButton = nullptr;
CartOverlayWidget* m_cartOverlay = nullptr;
inline constexpr static int s_iconSize = 24;
inline constexpr static int s_arrowDownIconSize = 8;
};
class GemCatalogHeaderWidget
: public QFrame
{
Q_OBJECT // AUTOMOC
public:
explicit GemCatalogHeaderWidget(GemSortFilterProxyModel* filterProxyModel, QWidget* parent = nullptr);
explicit GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, QWidget* parent = nullptr);
~GemCatalogHeaderWidget() = default;
void ReinitForProject();
private:
AzQtComponents::SearchLineEdit* m_filterLineEdit = nullptr;
inline constexpr static int s_height = 60;
};
} // namespace O3DE::ProjectManager

@ -12,7 +12,6 @@
#include <GemCatalog/GemCatalogScreen.h>
#include <PythonBindingsInterface.h>
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <GemCatalog/GemListHeaderWidget.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <QVBoxLayout>
@ -35,8 +34,8 @@ namespace O3DE::ProjectManager
vLayout->setSpacing(0);
setLayout(vLayout);
GemCatalogHeaderWidget* headerWidget = new GemCatalogHeaderWidget(m_proxModel);
vLayout->addWidget(headerWidget);
m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxModel);
vLayout->addWidget(m_headerWidget);
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setMargin(0);
@ -77,10 +76,11 @@ namespace O3DE::ProjectManager
m_filterWidget->deleteLater();
}
m_proxModel->ResetFilters();
m_filterWidget = new GemFilterWidget(m_proxModel);
m_filterWidgetLayout->addWidget(m_filterWidget);
m_proxModel->InvalidateFilter();
m_headerWidget->ReinitForProject();
// Select the first entry after everything got correctly sized
QTimer::singleShot(200, [=]{

@ -14,6 +14,7 @@
#if !defined(Q_MOC_RUN)
#include <ScreenWidget.h>
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <GemCatalog/GemFilterWidget.h>
#include <GemCatalog/GemListView.h>
#include <GemCatalog/GemInspector.h>
@ -40,6 +41,7 @@ namespace O3DE::ProjectManager
GemListView* m_gemListView = nullptr;
GemInspector* m_gemInspector = nullptr;
GemModel* m_gemModel = nullptr;
GemCatalogHeaderWidget* m_headerWidget = nullptr;
GemSortFilterProxyModel* m_proxModel = nullptr;
QVBoxLayout* m_filterWidgetLayout = nullptr;
GemFilterWidget* m_filterWidget = nullptr;

@ -53,6 +53,7 @@ namespace O3DE::ProjectManager
QFont standardFont(options.font);
standardFont.setPixelSize(s_fontSize);
QFontMetrics standardFontMetrics(standardFont);
painter->save();
painter->setClipping(true);
@ -78,8 +79,10 @@ namespace O3DE::ProjectManager
}
// Gem name
const QString gemName = GemModel::GetName(modelIndex);
QString gemName = GemModel::GetName(modelIndex);
QFont gemNameFont(options.font);
const int firstColumnMaxTextWidth = s_summaryStartX - 30;
gemName = QFontMetrics(gemNameFont).elidedText(gemName, Qt::TextElideMode::ElideRight, firstColumnMaxTextWidth);
gemNameFont.setPixelSize(s_gemNameFontSize);
gemNameFont.setBold(true);
QRect gemNameRect = GetTextRect(gemNameFont, gemName, s_gemNameFontSize);
@ -90,7 +93,8 @@ namespace O3DE::ProjectManager
painter->drawText(gemNameRect, Qt::TextSingleLine, gemName);
// Gem creator
const QString gemCreator = GemModel::GetCreator(modelIndex);
QString gemCreator = GemModel::GetCreator(modelIndex);
gemCreator = standardFontMetrics.elidedText(gemCreator, Qt::TextElideMode::ElideRight, firstColumnMaxTextWidth);
QRect gemCreatorRect = GetTextRect(standardFont, gemCreator, s_fontSize);
gemCreatorRect.moveTo(contentRect.left(), contentRect.top() + gemNameRect.height());

@ -130,4 +130,15 @@ namespace O3DE::ProjectManager
invalidate();
emit OnInvalidated();
}
void GemSortFilterProxyModel::ResetFilters()
{
m_searchString.clear();
m_gemOriginFilter = {};
m_platformFilter = {};
m_typeFilter = {};
m_featureFilter = {};
InvalidateFilter();
}
} // namespace O3DE::ProjectManager

@ -51,6 +51,7 @@ namespace O3DE::ProjectManager
void SetFeatures(const QSet<QString>& features) { m_featureFilter = features; InvalidateFilter(); }
void InvalidateFilter();
void ResetFilters();
signals:
void OnInvalidated();

@ -14,8 +14,12 @@
#include <PythonBindingsInterface.h>
#include <FormLineEditWidget.h>
#include <FormBrowseEditWidget.h>
#include <TemplateButtonWidget.h>
#include <PathValidator.h>
#include <EngineInfo.h>
#include <CreateProjectCtrl.h>
#include <TagWidget.h>
#include <AzQtComponents/Components/FlowLayout.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
@ -28,10 +32,12 @@
#include <QSpacerItem>
#include <QStandardPaths>
#include <QFrame>
#include <QScrollArea>
#include <QAbstractButton>
namespace O3DE::ProjectManager
{
constexpr const char* k_pathProperty = "Path";
constexpr const char* k_templateIndexProperty = "TemplateIndex";
NewProjectSettingsScreen::NewProjectSettingsScreen(QWidget* parent)
: ProjectSettingsScreen(parent)
@ -59,30 +65,69 @@ namespace O3DE::ProjectManager
projectTemplateDetailsLabel->setObjectName("projectTemplateDetailsLabel");
containerLayout->addWidget(projectTemplateDetailsLabel);
QHBoxLayout* templateLayout = new QHBoxLayout(this);
containerLayout->addItem(templateLayout);
// we might have enough templates that we need to scroll
QScrollArea* templatesScrollArea = new QScrollArea(this);
QWidget* scrollWidget = new QWidget();
FlowLayout* flowLayout = new FlowLayout(0, s_spacerSize, s_spacerSize);
scrollWidget->setLayout(flowLayout);
templatesScrollArea->setWidget(scrollWidget);
templatesScrollArea->setWidgetResizable(true);
m_projectTemplateButtonGroup = new QButtonGroup(this);
m_projectTemplateButtonGroup->setObjectName("templateButtonGroup");
// QButtonGroup has overloaded buttonClicked methods so we need the QOverload
connect(
m_projectTemplateButtonGroup, QOverload<QAbstractButton*>::of(&QButtonGroup::buttonClicked), this,
[=](QAbstractButton* button)
{
if (button && button->property(k_templateIndexProperty).isValid())
{
int projectIndex = button->property(k_templateIndexProperty).toInt();
UpdateTemplateDetails(m_templates.at(projectIndex));
}
});
auto templatesResult = PythonBindingsInterface::Get()->GetProjectTemplates();
if (templatesResult.IsSuccess() && !templatesResult.GetValue().isEmpty())
{
for (const ProjectTemplateInfo& projectTemplate : templatesResult.GetValue())
m_templates = templatesResult.GetValue();
// sort alphabetically by display name because they could be in any order
std::sort(m_templates.begin(), m_templates.end(), [](const ProjectTemplateInfo& arg1, const ProjectTemplateInfo& arg2)
{
QRadioButton* radioButton = new QRadioButton(projectTemplate.m_name, this);
radioButton->setProperty(k_pathProperty, projectTemplate.m_path);
m_projectTemplateButtonGroup->addButton(radioButton);
return arg1.m_displayName.toLower() < arg2.m_displayName.toLower();
});
containerLayout->addWidget(radioButton);
for (int index = 0; index < m_templates.size(); ++index)
{
ProjectTemplateInfo projectTemplate = m_templates.at(index);
QString projectPreviewPath = projectTemplate.m_path + "/Template/preview.png";
QFileInfo doesPreviewExist(projectPreviewPath);
if (!doesPreviewExist.exists() || !doesPreviewExist.isFile())
{
projectPreviewPath = ":/DefaultTemplate.png";
}
TemplateButton* templateButton = new TemplateButton(projectPreviewPath, projectTemplate.m_displayName, this);
templateButton->setCheckable(true);
templateButton->setProperty(k_templateIndexProperty, index);
m_projectTemplateButtonGroup->addButton(templateButton);
flowLayout->addWidget(templateButton);
}
m_projectTemplateButtonGroup->buttons().first()->setChecked(true);
}
containerLayout->addWidget(templatesScrollArea);
}
projectTemplateWidget->setLayout(containerLayout);
m_verticalLayout->addWidget(projectTemplateWidget);
QWidget* projectTemplateDetails = new QWidget(this);
QFrame* projectTemplateDetails = CreateTemplateDetails(s_templateDetailsContentMargin);
projectTemplateDetails->setObjectName("projectTemplateDetails");
m_horizontalLayout->addWidget(projectTemplateDetails);
}
@ -109,11 +154,71 @@ namespace O3DE::ProjectManager
void NewProjectSettingsScreen::NotifyCurrentScreen()
{
if (!m_templates.isEmpty())
{
UpdateTemplateDetails(m_templates.first());
}
Validate();
}
QString NewProjectSettingsScreen::GetProjectTemplatePath()
{
return m_projectTemplateButtonGroup->checkedButton()->property(k_pathProperty).toString();
const int templateIndex = m_projectTemplateButtonGroup->checkedButton()->property(k_templateIndexProperty).toInt();
return m_templates.at(templateIndex).m_path;
}
QFrame* NewProjectSettingsScreen::CreateTemplateDetails(int margin)
{
QFrame* projectTemplateDetails = new QFrame(this);
projectTemplateDetails->setObjectName("projectTemplateDetails");
QVBoxLayout* templateDetailsLayout = new QVBoxLayout();
templateDetailsLayout->setContentsMargins(margin, margin, margin, margin);
templateDetailsLayout->setAlignment(Qt::AlignTop);
{
m_templateDisplayName = new QLabel(this);
m_templateDisplayName->setObjectName("displayName");
templateDetailsLayout->addWidget(m_templateDisplayName);
m_templateSummary = new QLabel(this);
m_templateSummary->setObjectName("summary");
m_templateSummary->setWordWrap(true);
templateDetailsLayout->addWidget(m_templateSummary);
QLabel* includedGemsTitle = new QLabel(tr("Included Gems"), this);
includedGemsTitle->setObjectName("includedGemsTitle");
templateDetailsLayout->addWidget(includedGemsTitle);
m_templateIncludedGems = new TagContainerWidget(this);
m_templateIncludedGems->setObjectName("includedGems");
templateDetailsLayout->addWidget(m_templateIncludedGems);
#ifdef TEMPLATE_GEM_CONFIGURATION_ENABLED
QLabel* moreGemsLabel = new QLabel(tr("Looking for more Gems?"), this);
moreGemsLabel->setObjectName("moreGems");
templateDetailsLayout->addWidget(moreGemsLabel);
QLabel* browseCatalogLabel = new QLabel(tr("Browse the Gems Catalog to further customize your project."), this);
browseCatalogLabel->setObjectName("browseCatalog");
browseCatalogLabel->setWordWrap(true);
templateDetailsLayout->addWidget(browseCatalogLabel);
QPushButton* configureGemsButton = new QPushButton(tr("Configure with more Gems"), this);
connect(configureGemsButton, &QPushButton::clicked, this, [=]()
{
emit ChangeScreenRequest(ProjectManagerScreen::GemCatalog);
});
templateDetailsLayout->addWidget(configureGemsButton);
#endif // TEMPLATE_GEM_CONFIGURATION_ENABLED
}
projectTemplateDetails->setLayout(templateDetailsLayout);
return projectTemplateDetails;
}
void NewProjectSettingsScreen::UpdateTemplateDetails(const ProjectTemplateInfo& templateInfo)
{
m_templateDisplayName->setText(templateInfo.m_displayName);
m_templateSummary->setText(templateInfo.m_summary);
m_templateIncludedGems->Update(templateInfo.m_includedGems);
}
} // namespace O3DE::ProjectManager

@ -13,12 +13,17 @@
#if !defined(Q_MOC_RUN)
#include <ProjectSettingsScreen.h>
#include <ProjectTemplateInfo.h>
#include <QVector>
#endif
QT_FORWARD_DECLARE_CLASS(QButtonGroup)
QT_FORWARD_DECLARE_CLASS(QLabel)
QT_FORWARD_DECLARE_CLASS(QFrame)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(TagContainerWidget)
class NewProjectSettingsScreen
: public ProjectSettingsScreen
{
@ -33,8 +38,17 @@ namespace O3DE::ProjectManager
private:
QString GetDefaultProjectPath();
QFrame* CreateTemplateDetails(int margin);
void UpdateTemplateDetails(const ProjectTemplateInfo& templateInfo);
QButtonGroup* m_projectTemplateButtonGroup;
QLabel* m_templateDisplayName;
QLabel* m_templateSummary;
TagContainerWidget* m_templateIncludedGems;
QVector<ProjectTemplateInfo> m_templates;
inline constexpr static int s_spacerSize = 20;
inline constexpr static int s_templateDetailsContentMargin = 20;
};
} // namespace O3DE::ProjectManager

@ -0,0 +1,250 @@
/*
* 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 <ProjectBuilder.h>
#include <ProjectButtonWidget.h>
#include <PythonBindingsInterface.h>
#include <QProcess>
#include <QFile>
#include <QTextStream>
#include <QMessageBox>
#include <QDesktopServices>
#include <QUrl>
#include <QDir>
#include <QProcessEnvironment>
//#define MOCK_BUILD_PROJECT true
namespace O3DE::ProjectManager
{
// 10 Minutes
constexpr int MaxBuildTimeMSecs = 600000;
static const QString BuildPathPostfix = "windows_vs2019";
static const QString ErrorLogPathPostfix = "CMakeFiles/CMakeProjectBuildError.log";
ProjectBuilderWorker::ProjectBuilderWorker(const ProjectInfo& projectInfo)
: QObject()
, m_projectInfo(projectInfo)
{
}
void ProjectBuilderWorker::BuildProject()
{
#ifdef MOCK_BUILD_PROJECT
for (int i = 0; i < 10; ++i)
{
QThread::sleep(1);
UpdateProgress(i * 10);
}
Done(m_projectPath);
#else
EngineInfo engineInfo;
AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
if (engineInfoResult.IsSuccess())
{
engineInfo = engineInfoResult.GetValue();
}
else
{
emit Done(tr("Failed to get engine info."));
return;
}
// Show some kind of progress with very approximate estimates
UpdateProgress(1);
QProcessEnvironment currentEnvironment(QProcessEnvironment::systemEnvironment());
// Append cmake path to PATH incase it is missing
QDir cmakePath(engineInfo.m_path);
cmakePath.cd("cmake/runtime/bin");
QString pathValue = currentEnvironment.value("PATH");
pathValue += ";" + cmakePath.path();
currentEnvironment.insert("PATH", pathValue);
QProcess configProjectProcess;
configProjectProcess.setProcessChannelMode(QProcess::MergedChannels);
configProjectProcess.setWorkingDirectory(m_projectInfo.m_path);
configProjectProcess.setProcessEnvironment(currentEnvironment);
configProjectProcess.start(
"cmake",
QStringList
{
"-B",
QDir(m_projectInfo.m_path).filePath(BuildPathPostfix),
"-S",
m_projectInfo.m_path,
"-G",
"Visual Studio 16",
"-DLY_3RDPARTY_PATH=" + engineInfo.m_thirdPartyPath
});
if (!configProjectProcess.waitForStarted())
{
emit Done(tr("Configuring project failed to start."));
return;
}
if (!configProjectProcess.waitForFinished(MaxBuildTimeMSecs))
{
WriteErrorLog(configProjectProcess.readAllStandardOutput());
emit Done(tr("Configuring project timed out. See log for details"));
return;
}
QString configProjectOutput(configProjectProcess.readAllStandardOutput());
if (configProjectProcess.exitCode() != 0 || !configProjectOutput.contains("Generating done"))
{
WriteErrorLog(configProjectOutput);
emit Done(tr("Configuring project failed. See log for details."));
return;
}
UpdateProgress(20);
QProcess buildProjectProcess;
buildProjectProcess.setProcessChannelMode(QProcess::MergedChannels);
buildProjectProcess.setWorkingDirectory(m_projectInfo.m_path);
buildProjectProcess.setProcessEnvironment(currentEnvironment);
buildProjectProcess.start(
"cmake",
QStringList
{
"--build",
QDir(m_projectInfo.m_path).filePath(BuildPathPostfix),
"--target",
m_projectInfo.m_projectName + ".GameLauncher",
"Editor",
"--config",
"profile"
});
if (!buildProjectProcess.waitForStarted())
{
emit Done(tr("Building project failed to start."));
return;
}
if (!buildProjectProcess.waitForFinished(MaxBuildTimeMSecs))
{
WriteErrorLog(configProjectProcess.readAllStandardOutput());
emit Done(tr("Building project timed out. See log for details"));
return;
}
QString buildProjectOutput(buildProjectProcess.readAllStandardOutput());
if (configProjectProcess.exitCode() != 0)
{
WriteErrorLog(buildProjectOutput);
emit Done(tr("Building project failed. See log for details."));
}
else
{
emit Done("");
}
#endif
}
QString ProjectBuilderWorker::LogFilePath() const
{
QDir logFilePath(m_projectInfo.m_path);
logFilePath.cd(BuildPathPostfix);
return logFilePath.filePath(ErrorLogPathPostfix);
}
void ProjectBuilderWorker::WriteErrorLog(const QString& log)
{
QFile logFile(LogFilePath());
// Overwrite file with truncate
if (logFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text))
{
QTextStream output(&logFile);
output << log;
logFile.close();
}
}
ProjectBuilderController::ProjectBuilderController(const ProjectInfo& projectInfo, ProjectButton* projectButton, QWidget* parent)
: QObject()
, m_projectInfo(projectInfo)
, m_projectButton(projectButton)
, m_parent(parent)
{
m_worker = new ProjectBuilderWorker(m_projectInfo);
m_worker->moveToThread(&m_workerThread);
connect(&m_workerThread, &QThread::finished, m_worker, &ProjectBuilderWorker::deleteLater);
connect(&m_workerThread, &QThread::started, m_worker, &ProjectBuilderWorker::BuildProject);
connect(m_worker, &ProjectBuilderWorker::Done, this, &ProjectBuilderController::HandleResults);
connect(m_worker, &ProjectBuilderWorker::UpdateProgress, this, &ProjectBuilderController::UpdateUIProgress);
}
ProjectBuilderController::~ProjectBuilderController()
{
m_workerThread.quit();
m_workerThread.wait();
}
void ProjectBuilderController::Start()
{
m_workerThread.start();
UpdateUIProgress(0);
}
void ProjectBuilderController::SetProjectButton(ProjectButton* projectButton)
{
m_projectButton = projectButton;
}
QString ProjectBuilderController::GetProjectPath() const
{
return m_projectInfo.m_path;
}
void ProjectBuilderController::UpdateUIProgress(int progress)
{
if (m_projectButton)
{
m_projectButton->SetButtonOverlayText(QString("%1 (%2%)\n\n").arg(tr("Building Project..."), QString::number(progress)));
m_projectButton->SetProgressBarValue(progress);
}
}
void ProjectBuilderController::HandleResults(const QString& result)
{
if (!result.isEmpty())
{
if (result.contains(tr("log")))
{
QMessageBox::StandardButton openLog = QMessageBox::critical(
m_parent,
tr("Project Failed to Build!"),
result + tr("\n\nWould you like to view log?"),
QMessageBox::No | QMessageBox::Yes);
if (openLog == QMessageBox::Yes)
{
// Open application assigned to this file type
QDesktopServices::openUrl(QUrl("file:///" + m_worker->LogFilePath()));
}
}
else
{
QMessageBox::critical(m_parent, tr("Project Failed to Build!"), result);
}
}
emit Done();
}
} // namespace O3DE::ProjectManager

@ -0,0 +1,73 @@
/*
* 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
#if !defined(Q_MOC_RUN)
#include <ProjectInfo.h>
#include <QThread>
#endif
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(ProjectButton)
class ProjectBuilderWorker : public QObject
{
Q_OBJECT
public:
explicit ProjectBuilderWorker(const ProjectInfo& projectInfo);
~ProjectBuilderWorker() = default;
QString LogFilePath() const;
public slots:
void BuildProject();
signals:
void UpdateProgress(int progress);
void Done(QString result);
private:
void WriteErrorLog(const QString& log);
ProjectInfo m_projectInfo;
};
class ProjectBuilderController : public QObject
{
Q_OBJECT
public:
explicit ProjectBuilderController(const ProjectInfo& projectInfo, ProjectButton* projectButton, QWidget* parent = nullptr);
~ProjectBuilderController();
void SetProjectButton(ProjectButton* projectButton);
QString GetProjectPath() const;
public slots:
void Start();
void UpdateUIProgress(int progress);
void HandleResults(const QString& result);
signals:
void Done();
private:
ProjectInfo m_projectInfo;
ProjectBuilderWorker* m_worker;
QThread m_workerThread;
ProjectButton* m_projectButton;
QWidget* m_parent;
};
} // namespace O3DE::ProjectManager

@ -21,6 +21,7 @@
#include <QPixmap>
#include <QMenu>
#include <QSpacerItem>
#include <QProgressBar>
namespace O3DE::ProjectManager
{
@ -31,11 +32,26 @@ namespace O3DE::ProjectManager
: QLabel(parent)
{
setObjectName("labelButton");
QVBoxLayout* vLayout = new QVBoxLayout(this);
vLayout->setContentsMargins(0, 0, 0, 0);
vLayout->setSpacing(5);
setLayout(vLayout);
m_overlayLabel = new QLabel("", this);
m_overlayLabel->setObjectName("labelButtonOverlay");
m_overlayLabel->setWordWrap(true);
m_overlayLabel->setAlignment(Qt::AlignCenter);
m_overlayLabel->setVisible(false);
vLayout->addWidget(m_overlayLabel);
m_buildButton = new QPushButton(tr("Build Project"), this);
m_buildButton->setVisible(false);
m_progressBar = new QProgressBar(this);
m_progressBar->setObjectName("labelButtonProgressBar");
m_progressBar->setVisible(false);
vLayout->addWidget(m_progressBar);
}
void LabelButton::mousePressEvent([[maybe_unused]] QMouseEvent* event)
@ -57,7 +73,22 @@ namespace O3DE::ProjectManager
m_overlayLabel->setText(text);
}
ProjectButton::ProjectButton(const ProjectInfo& projectInfo, QWidget* parent)
QLabel* LabelButton::GetOverlayLabel()
{
return m_overlayLabel;
}
QProgressBar* LabelButton::GetProgressBar()
{
return m_progressBar;
}
QPushButton* LabelButton::GetBuildButton()
{
return m_buildButton;
}
ProjectButton::ProjectButton(const ProjectInfo& projectInfo, QWidget* parent, bool processing)
: QFrame(parent)
, m_projectInfo(projectInfo)
{
@ -66,10 +97,18 @@ namespace O3DE::ProjectManager
m_projectInfo.m_imagePath = ":/DefaultProjectImage.png";
}
Setup();
BaseSetup();
if (processing)
{
ProcessingSetup();
}
else
{
ReadySetup();
}
}
void ProjectButton::Setup()
void ProjectButton::BaseSetup()
{
setObjectName("projectButton");
@ -87,8 +126,37 @@ namespace O3DE::ProjectManager
m_projectImageLabel->setPixmap(
QPixmap(m_projectInfo.m_imagePath).scaled(m_projectImageLabel->size(), Qt::KeepAspectRatioByExpanding));
m_projectFooter = new QFrame(this);
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setContentsMargins(0, 0, 0, 0);
m_projectFooter->setLayout(hLayout);
{
QLabel* projectNameLabel = new QLabel(m_projectInfo.m_displayName, this);
hLayout->addWidget(projectNameLabel);
}
vLayout->addWidget(m_projectFooter);
}
void ProjectButton::ProcessingSetup()
{
m_projectImageLabel->GetOverlayLabel()->setAlignment(Qt::AlignHCenter | Qt::AlignBottom);
m_projectImageLabel->SetEnabled(false);
m_projectImageLabel->SetOverlayText(tr("Processing...\n\n"));
QProgressBar* progressBar = m_projectImageLabel->GetProgressBar();
progressBar->setVisible(true);
progressBar->setValue(0);
}
void ProjectButton::ReadySetup()
{
connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectInfo.m_path); });
connect(m_projectImageLabel->GetBuildButton(), &QPushButton::clicked, [this](){ emit BuildProject(m_projectInfo); });
QMenu* menu = new QMenu(this);
menu->addAction(tr("Edit Project Settings..."), this, [this]() { emit EditProject(m_projectInfo.m_path); });
menu->addAction(tr("Build"), this, [this]() { emit BuildProject(m_projectInfo); });
menu->addSeparator();
menu->addAction(tr("Open Project folder..."), this, [this]()
{
@ -100,30 +168,33 @@ namespace O3DE::ProjectManager
menu->addAction(tr("Remove from O3DE"), this, [this]() { emit RemoveProject(m_projectInfo.m_path); });
menu->addAction(tr("Delete this Project"), this, [this]() { emit DeleteProject(m_projectInfo.m_path); });
QFrame* footer = new QFrame(this);
QHBoxLayout* hLayout = new QHBoxLayout();
hLayout->setContentsMargins(0, 0, 0, 0);
footer->setLayout(hLayout);
{
QLabel* projectNameLabel = new QLabel(m_projectInfo.m_displayName, this);
hLayout->addWidget(projectNameLabel);
QPushButton* projectMenuButton = new QPushButton(this);
projectMenuButton->setObjectName("projectMenuButton");
projectMenuButton->setMenu(menu);
hLayout->addWidget(projectMenuButton);
}
vLayout->addWidget(footer);
QPushButton* projectMenuButton = new QPushButton(this);
projectMenuButton->setObjectName("projectMenuButton");
projectMenuButton->setMenu(menu);
m_projectFooter->layout()->addWidget(projectMenuButton);
}
void ProjectButton::SetButtonEnabled(bool enabled)
void ProjectButton::SetLaunchButtonEnabled(bool enabled)
{
m_projectImageLabel->SetEnabled(enabled);
}
void ProjectButton::ShowBuildButton(bool show)
{
QSpacerItem* buttonSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding);
m_projectImageLabel->layout()->addItem(buttonSpacer);
m_projectImageLabel->layout()->addWidget(m_projectImageLabel->GetBuildButton());
m_projectImageLabel->GetBuildButton()->setVisible(show);
}
void ProjectButton::SetButtonOverlayText(const QString& text)
{
m_projectImageLabel->SetOverlayText(text);
}
void ProjectButton::SetProgressBarValue(int progress)
{
m_projectImageLabel->GetProgressBar()->setValue(progress);
}
} // namespace O3DE::ProjectManager

@ -21,6 +21,7 @@
QT_FORWARD_DECLARE_CLASS(QPixmap)
QT_FORWARD_DECLARE_CLASS(QPushButton)
QT_FORWARD_DECLARE_CLASS(QAction)
QT_FORWARD_DECLARE_CLASS(QProgressBar)
namespace O3DE::ProjectManager
{
@ -36,6 +37,10 @@ namespace O3DE::ProjectManager
void SetEnabled(bool enabled);
void SetOverlayText(const QString& text);
QLabel* GetOverlayLabel();
QProgressBar* GetProgressBar();
QPushButton* GetBuildButton();
signals:
void triggered();
@ -44,6 +49,8 @@ namespace O3DE::ProjectManager
private:
QLabel* m_overlayLabel;
QProgressBar* m_progressBar;
QPushButton* m_buildButton;
bool m_enabled = true;
};
@ -53,11 +60,13 @@ namespace O3DE::ProjectManager
Q_OBJECT // AUTOMOC
public:
explicit ProjectButton(const ProjectInfo& m_projectInfo, QWidget* parent = nullptr);
explicit ProjectButton(const ProjectInfo& m_projectInfo, QWidget* parent = nullptr, bool processing = false);
~ProjectButton() = default;
void SetButtonEnabled(bool enabled);
void SetLaunchButtonEnabled(bool enabled);
void ShowBuildButton(bool show);
void SetButtonOverlayText(const QString& text);
void SetProgressBarValue(int progress);
signals:
void OpenProject(const QString& projectName);
@ -65,11 +74,15 @@ namespace O3DE::ProjectManager
void CopyProject(const QString& projectName);
void RemoveProject(const QString& projectName);
void DeleteProject(const QString& projectName);
void BuildProject(const ProjectInfo& projectInfo);
private:
void Setup();
void BaseSetup();
void ProcessingSetup();
void ReadySetup();
ProjectInfo m_projectInfo;
LabelButton* m_projectImageLabel;
QFrame* m_projectFooter;
};
} // namespace O3DE::ProjectManager

@ -24,8 +24,10 @@ namespace O3DE::ProjectManager
{
public:
ProjectInfo() = default;
ProjectInfo(const QString& path, const QString& projectName, const QString& displayName, const QString& origin,
const QString& summary, const QString& imagePath, const QString& backgroundImagePath, bool needsBuild);
const QString& summary, const QString& imagePath, const QString& backgroundImagePath, bool needsBuild
bool operator==(const ProjectInfo& rhs);
bool operator!=(const ProjectInfo& rhs);
@ -46,9 +48,10 @@ namespace O3DE::ProjectManager
QString m_backgroundImagePath;
// Used in project creation
bool m_needsBuild = false; //! Is this a new project or existing
// Used to flag tags for removal
QStringList m_userTagsForRemoval;
bool m_needsBuild = false; //! Does this project need to be built
};
} // namespace O3DE::ProjectManager

@ -31,6 +31,7 @@ namespace O3DE::ProjectManager
QString m_name;
QString m_path;
QString m_summary;
QStringList m_includedGems;
QStringList m_canonicalTags;
QStringList m_userTags;
};

@ -16,7 +16,9 @@
#include <QFileDialog>
#include <QDir>
#include <QMessageBox>
#include <QProgressDialog>
#include <QFileInfo>
#include <QProcess>
#include <QProcessEnvironment>
namespace O3DE::ProjectManager
{
@ -192,6 +194,49 @@ namespace O3DE::ProjectManager
return true;
}
static bool IsVS2019Installed_internal()
{
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
QString programFilesPath = environment.value("ProgramFiles(x86)");
QString vsWherePath = programFilesPath + "\\Microsoft Visual Studio\\Installer\\vswhere.exe";
QFileInfo vsWhereFile(vsWherePath);
if (vsWhereFile.exists() && vsWhereFile.isFile())
{
QProcess vsWhereProcess;
vsWhereProcess.setProcessChannelMode(QProcess::MergedChannels);
vsWhereProcess.start(
vsWherePath,
QStringList{ "-version", "16.0", "-latest", "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
"-property", "isComplete" });
if (!vsWhereProcess.waitForStarted())
{
return false;
}
while (vsWhereProcess.waitForReadyRead())
{
}
QString vsWhereOutput(vsWhereProcess.readAllStandardOutput());
if (vsWhereOutput.startsWith("1"))
{
return true;
}
}
return false;
}
bool IsVS2019Installed()
{
static bool vs2019Installed = IsVS2019Installed_internal();
return vs2019Installed;
}
ProjectManagerScreen GetProjectManagerScreen(const QString& screen)
{
auto iter = s_ProjectManagerStringNames.find(screen);
@ -202,6 +247,5 @@ namespace O3DE::ProjectManager
return ProjectManagerScreen::Invalid;
}
} // namespace ProjectUtils
} // namespace O3DE::ProjectManager

@ -25,6 +25,9 @@ namespace O3DE::ProjectManager
bool CopyProject(const QString& origPath, const QString& newPath);
bool DeleteProjectFiles(const QString& path, bool force = false);
bool MoveProject(const QString& origPath, const QString& newPath, QWidget* parent = nullptr);
bool IsVS2019Installed();
ProjectManagerScreen GetProjectManagerScreen(const QString& screen);
} // namespace ProjectUtils
} // namespace O3DE::ProjectManager

@ -15,6 +15,8 @@
#include <ProjectButtonWidget.h>
#include <PythonBindingsInterface.h>
#include <ProjectUtils.h>
#include <ProjectBuilder.h>
#include <ScreensCtrl.h>
#include <AzQtComponents/Components/FlowLayout.h>
#include <AzCore/Platform.h>
@ -42,6 +44,8 @@
#include <QSettings>
#include <QMessageBox>
#include <QTimer>
#include <QQueue>
#include <QDir>
//#define DISPLAY_PROJECT_DEV_DATA true
@ -66,6 +70,14 @@ namespace O3DE::ProjectManager
m_stack->addWidget(m_projectsContent);
vLayout->addWidget(m_stack);
connect(reinterpret_cast<ScreensCtrl*>(parent), &ScreensCtrl::NotifyBuildProject, this, &ProjectsScreen::SuggestBuildProject);
}
ProjectsScreen::~ProjectsScreen()
{
delete m_currentBuilder;
}
QFrame* ProjectsScreen::CreateFirstTimeContent()
@ -110,7 +122,7 @@ namespace O3DE::ProjectManager
return frame;
}
QFrame* ProjectsScreen::CreateProjectsContent()
QFrame* ProjectsScreen::CreateProjectsContent(QString buildProjectPath, ProjectButton** projectButton)
{
QFrame* frame = new QFrame(this);
frame->setObjectName("projectsContent");
@ -158,30 +170,43 @@ namespace O3DE::ProjectManager
projectsScrollArea->setWidgetResizable(true);
#ifndef DISPLAY_PROJECT_DEV_DATA
// Iterate once to insert building project first
if (!buildProjectPath.isEmpty())
{
buildProjectPath = QDir::fromNativeSeparators(buildProjectPath);
for (auto project : projectsResult.GetValue())
{
if (QDir::fromNativeSeparators(project.m_path) == buildProjectPath)
{
ProjectButton* buildingProjectButton = CreateProjectButton(project, flowLayout, true);
if (projectButton)
{
*projectButton = buildingProjectButton;
}
break;
}
}
}
for (auto project : projectsResult.GetValue())
#else
ProjectInfo project = projectsResult.GetValue().at(0);
for (int i = 0; i < 15; i++)
#endif
{
ProjectButton* projectButton;
QString projectPreviewPath = project.m_path + m_projectPreviewImagePath;
QFileInfo doesPreviewExist(projectPreviewPath);
if (doesPreviewExist.exists() && doesPreviewExist.isFile())
// Add all other projects skipping building project
// Safe if no building project because it is just an empty string
if (project.m_path != buildProjectPath)
{
project.m_imagePath = projectPreviewPath;
}
projectButton = new ProjectButton(project, this);
flowLayout->addWidget(projectButton);
ProjectButton* projectButtonWidget = CreateProjectButton(project, flowLayout);
connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject);
connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject);
connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject);
connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject);
connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject);
if (RequiresBuildProjectIterator(project.m_path) != m_requiresBuild.end())
{
projectButtonWidget->ShowBuildButton(true);
}
}
}
layout->addWidget(projectsScrollArea);
@ -191,6 +216,60 @@ namespace O3DE::ProjectManager
return frame;
}
ProjectButton* ProjectsScreen::CreateProjectButton(ProjectInfo& project, QLayout* flowLayout, bool processing)
{
ProjectButton* projectButton;
QString projectPreviewPath = project.m_path + m_projectPreviewImagePath;
QFileInfo doesPreviewExist(projectPreviewPath);
if (doesPreviewExist.exists() && doesPreviewExist.isFile())
{
project.m_imagePath = projectPreviewPath;
}
projectButton = new ProjectButton(project, this, processing);
flowLayout->addWidget(projectButton);
if (!processing)
{
connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject);
connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject);
connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject);
connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject);
connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject);
}
connect(projectButton, &ProjectButton::BuildProject, this, &ProjectsScreen::QueueBuildProject);
return projectButton;
}
void ProjectsScreen::ResetProjectsContent()
{
// refresh the projects content by re-creating it for now
if (m_projectsContent)
{
m_stack->removeWidget(m_projectsContent);
m_projectsContent->deleteLater();
}
// Make sure to update builder with latest Project Button
if (m_currentBuilder)
{
ProjectButton* projectButtonPtr;
m_projectsContent = CreateProjectsContent(m_currentBuilder->GetProjectPath(), &projectButtonPtr);
m_currentBuilder->SetProjectButton(projectButtonPtr);
}
else
{
m_projectsContent = CreateProjectsContent();
}
m_stack->addWidget(m_projectsContent);
m_stack->setCurrentWidget(m_projectsContent);
}
ProjectManagerScreen ProjectsScreen::GetScreenEnum()
{
return ProjectManagerScreen::Projects;
@ -237,7 +316,7 @@ namespace O3DE::ProjectManager
{
if (ProjectUtils::AddProjectDialog(this))
{
emit ResetScreenRequest(ProjectManagerScreen::Projects);
ResetProjectsContent();
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
}
@ -245,38 +324,47 @@ namespace O3DE::ProjectManager
{
if (!projectPath.isEmpty())
{
AZ::IO::FixedMaxPath executableDirectory = AZ::Utils::GetExecutableDirectory();
AZStd::string executableFilename = "Editor";
AZ::IO::FixedMaxPath editorExecutablePath = executableDirectory / (executableFilename + AZ_TRAIT_OS_EXECUTABLE_EXTENSION);
auto cmdPath = AZ::IO::FixedMaxPathString::format("%s -regset=\"/Amazon/AzCore/Bootstrap/project_path=%s\"", editorExecutablePath.c_str(), projectPath.toStdString().c_str());
AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo;
processLaunchInfo.m_commandlineParameters = cmdPath;
bool launchSucceeded = AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo);
if (!launchSucceeded)
if (!WarnIfInBuildQueue(projectPath))
{
AZ_Error("ProjectManager", false, "Failed to launch editor");
QMessageBox::critical( this, tr("Error"), tr("Failed to launch the Editor, please verify the project settings are valid."));
}
else
{
// prevent the user from accidentally pressing the button while the editor is launching
// and let them know what's happening
ProjectButton* button = qobject_cast<ProjectButton*>(sender());
if (button)
AZ::IO::FixedMaxPath executableDirectory = AZ::Utils::GetExecutableDirectory();
AZStd::string executableFilename = "Editor";
AZ::IO::FixedMaxPath editorExecutablePath = executableDirectory / (executableFilename + AZ_TRAIT_OS_EXECUTABLE_EXTENSION);
auto cmdPath = AZ::IO::FixedMaxPathString::format(
"%s -regset=\"/Amazon/AzCore/Bootstrap/project_path=%s\"", editorExecutablePath.c_str(),
projectPath.toStdString().c_str());
AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo;
processLaunchInfo.m_commandlineParameters = cmdPath;
bool launchSucceeded = AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo);
if (!launchSucceeded)
{
button->SetButtonEnabled(false);
button->SetButtonOverlayText(tr("Opening Editor..."));
AZ_Error("ProjectManager", false, "Failed to launch editor");
QMessageBox::critical(
this, tr("Error"), tr("Failed to launch the Editor, please verify the project settings are valid."));
}
else
{
// prevent the user from accidentally pressing the button while the editor is launching
// and let them know what's happening
ProjectButton* button = qobject_cast<ProjectButton*>(sender());
if (button)
{
button->SetLaunchButtonEnabled(false);
button->SetButtonOverlayText(tr("Opening Editor..."));
}
// enable the button after 3 seconds
constexpr int waitTimeInMs = 3000;
QTimer::singleShot(waitTimeInMs, this, [this, button] {
if (button)
// enable the button after 3 seconds
constexpr int waitTimeInMs = 3000;
QTimer::singleShot(
waitTimeInMs, this,
[this, button]
{
button->SetButtonEnabled(true);
}
});
if (button)
{
button->SetLaunchButtonEnabled(true);
}
});
}
}
}
else
@ -288,38 +376,90 @@ namespace O3DE::ProjectManager
}
void ProjectsScreen::HandleEditProject(const QString& projectPath)
{
emit NotifyCurrentProject(projectPath);
emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
if (!WarnIfInBuildQueue(projectPath))
{
emit NotifyCurrentProject(projectPath);
emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
}
}
void ProjectsScreen::HandleCopyProject(const QString& projectPath)
{
// Open file dialog and choose location for copied project then register copy with O3DE
if (ProjectUtils::CopyProjectDialog(projectPath, this))
if (!WarnIfInBuildQueue(projectPath))
{
emit ResetScreenRequest(ProjectManagerScreen::Projects);
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
// Open file dialog and choose location for copied project then register copy with O3DE
if (ProjectUtils::CopyProjectDialog(projectPath, this))
{
ResetProjectsContent();
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
}
}
void ProjectsScreen::HandleRemoveProject(const QString& projectPath)
{
// Unregister Project from O3DE and reload projects
if (ProjectUtils::UnregisterProject(projectPath))
if (!WarnIfInBuildQueue(projectPath))
{
emit ResetScreenRequest(ProjectManagerScreen::Projects);
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
// Unregister Project from O3DE and reload projects
if (ProjectUtils::UnregisterProject(projectPath))
{
ResetProjectsContent();
emit ChangeScreenRequest(ProjectManagerScreen::Projects);
}
}
}
void ProjectsScreen::HandleDeleteProject(const QString& projectPath)
{
QMessageBox::StandardButton warningResult = QMessageBox::warning(
this, tr("Delete Project"), tr("Are you sure?\nProject will be removed from O3DE and directory will be deleted!"),
QMessageBox::No | QMessageBox::Yes);
if (!WarnIfInBuildQueue(projectPath))
{
QMessageBox::StandardButton warningResult = QMessageBox::warning(this,
tr("Delete Project"),
tr("Are you sure?\nProject will be unregistered from O3DE and project directory will be deleted from your disk."),
QMessageBox::No | QMessageBox::Yes);
if (warningResult == QMessageBox::Yes)
{
// Remove project from O3DE and delete from disk
HandleRemoveProject(projectPath);
ProjectUtils::DeleteProjectFiles(projectPath);
}
}
}
if (warningResult == QMessageBox::Yes)
void ProjectsScreen::SuggestBuildProject(const ProjectInfo& projectInfo)
{
if (projectInfo.m_needsBuild)
{
// Remove project from O3DE and delete from disk
HandleRemoveProject(projectPath);
ProjectUtils::DeleteProjectFiles(projectPath);
if (RequiresBuildProjectIterator(projectInfo.m_path) == m_requiresBuild.end())
{
m_requiresBuild.append(projectInfo);
}
ResetProjectsContent();
}
else
{
QMessageBox::information(this,
tr("Project Should be rebuilt."),
projectInfo.m_projectName + tr(" project likely needs to be rebuilt."));
}
}
void ProjectsScreen::QueueBuildProject(const ProjectInfo& projectInfo)
{
auto requiredIter = RequiresBuildProjectIterator(projectInfo.m_path);
if (requiredIter != m_requiresBuild.end())
{
m_requiresBuild.erase(requiredIter);
}
if (!BuildQueueContainsProject(projectInfo.m_path))
{
if (m_buildQueue.empty() && !m_currentBuilder)
{
StartProjectBuild(projectInfo);
}
else
{
m_buildQueue.append(projectInfo);
}
}
}
@ -331,17 +471,7 @@ namespace O3DE::ProjectManager
}
else
{
// refresh the projects content by re-creating it for now
if (m_projectsContent)
{
m_stack->removeWidget(m_projectsContent);
m_projectsContent->deleteLater();
}
m_projectsContent = CreateProjectsContent();
m_stack->addWidget(m_projectsContent);
m_stack->setCurrentWidget(m_projectsContent);
ResetProjectsContent();
}
}
@ -363,4 +493,89 @@ namespace O3DE::ProjectManager
return displayFirstTimeContent;
}
void ProjectsScreen::StartProjectBuild(const ProjectInfo& projectInfo)
{
if (ProjectUtils::IsVS2019Installed())
{
QMessageBox::StandardButton buildProject = QMessageBox::information(
this,
tr("Building \"%1\"").arg(projectInfo.m_projectName),
tr("Ready to build \"%1\"?").arg(projectInfo.m_projectName),
QMessageBox::No | QMessageBox::Yes);
if (buildProject == QMessageBox::Yes)
{
m_currentBuilder = new ProjectBuilderController(projectInfo, nullptr, this);
ResetProjectsContent();
connect(m_currentBuilder, &ProjectBuilderController::Done, this, &ProjectsScreen::ProjectBuildDone);
m_currentBuilder->Start();
}
else
{
ProjectBuildDone();
}
}
}
void ProjectsScreen::ProjectBuildDone()
{
delete m_currentBuilder;
m_currentBuilder = nullptr;
if (!m_buildQueue.empty())
{
StartProjectBuild(m_buildQueue.front());
m_buildQueue.pop_front();
}
else
{
ResetProjectsContent();
}
}
QList<ProjectInfo>::iterator ProjectsScreen::RequiresBuildProjectIterator(const QString& projectPath)
{
QString nativeProjPath(QDir::toNativeSeparators(projectPath));
auto projectIter = m_requiresBuild.begin();
for (; projectIter != m_requiresBuild.end(); ++projectIter)
{
if (QDir::toNativeSeparators(projectIter->m_path) == nativeProjPath)
{
break;
}
}
return projectIter;
}
bool ProjectsScreen::BuildQueueContainsProject(const QString& projectPath)
{
QString nativeProjPath(QDir::toNativeSeparators(projectPath));
for (const ProjectInfo& project : m_buildQueue)
{
if (QDir::toNativeSeparators(project.m_path) == nativeProjPath)
{
return true;
}
}
return false;
}
bool ProjectsScreen::WarnIfInBuildQueue(const QString& projectPath)
{
if (BuildQueueContainsProject(projectPath))
{
QMessageBox::warning(
this,
tr("Action Temporarily Disabled!"),
tr("Action not allowed on projects in build queue."));
return true;
}
return false;
}
} // namespace O3DE::ProjectManager

@ -13,21 +13,28 @@
#if !defined(Q_MOC_RUN)
#include <ScreenWidget.h>
#include <ProjectInfo.h>
#include <QQueue>
#endif
QT_FORWARD_DECLARE_CLASS(QPaintEvent)
QT_FORWARD_DECLARE_CLASS(QFrame)
QT_FORWARD_DECLARE_CLASS(QStackedWidget)
QT_FORWARD_DECLARE_CLASS(QLayout)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(ProjectBuilderController);
QT_FORWARD_DECLARE_CLASS(ProjectButton);
class ProjectsScreen
: public ScreenWidget
{
public:
explicit ProjectsScreen(QWidget* parent = nullptr);
~ProjectsScreen() = default;
~ProjectsScreen();
ProjectManagerScreen GetScreenEnum() override;
QString GetTabText() override;
@ -35,6 +42,7 @@ namespace O3DE::ProjectManager
protected:
void NotifyCurrentScreen() override;
void ProjectBuildDone();
protected slots:
void HandleNewProjectButton();
@ -45,19 +53,32 @@ namespace O3DE::ProjectManager
void HandleRemoveProject(const QString& projectPath);
void HandleDeleteProject(const QString& projectPath);
void SuggestBuildProject(const ProjectInfo& projectInfo);
void QueueBuildProject(const ProjectInfo& projectInfo);
void paintEvent(QPaintEvent* event) override;
private:
QFrame* CreateFirstTimeContent();
QFrame* CreateProjectsContent();
QFrame* CreateProjectsContent(QString buildProjectPath = "", ProjectButton** projectButton = nullptr);
ProjectButton* CreateProjectButton(ProjectInfo& project, QLayout* flowLayout, bool processing = false);
void ResetProjectsContent();
bool ShouldDisplayFirstTimeContent();
QAction* m_createNewProjectAction;
QAction* m_addExistingProjectAction;
void StartProjectBuild(const ProjectInfo& projectInfo);
QList<ProjectInfo>::iterator RequiresBuildProjectIterator(const QString& projectPath);
bool BuildQueueContainsProject(const QString& projectPath);
bool WarnIfInBuildQueue(const QString& projectPath);
QAction* m_createNewProjectAction = nullptr;
QAction* m_addExistingProjectAction = nullptr;
QPixmap m_background;
QFrame* m_firstTimeContent;
QFrame* m_projectsContent;
QStackedWidget* m_stack;
QFrame* m_firstTimeContent = nullptr;
QFrame* m_projectsContent = nullptr;
QStackedWidget* m_stack = nullptr;
QList<ProjectInfo> m_requiresBuild;
QQueue<ProjectInfo> m_buildQueue;
ProjectBuilderController* m_currentBuilder = nullptr;
const QString m_projectPreviewImagePath = "/preview.png";

@ -669,7 +669,7 @@ namespace O3DE::ProjectManager
{
ProjectInfo projectInfo;
projectInfo.m_path = Py_To_String(path);
projectInfo.m_isNew = false;
projectInfo.m_needsBuild = false;
auto projectData = m_manifest.attr("get_project_json_data")(pybind11::none(), path);
if (pybind11::isinstance<pybind11::dict>(projectData))
@ -779,7 +779,7 @@ namespace O3DE::ProjectManager
ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromPath(pybind11::handle path)
{
ProjectTemplateInfo templateInfo;
templateInfo.m_path = Py_To_String(path);
templateInfo.m_path = Py_To_String(pybind11::str(path));
auto data = m_manifest.attr("get_template_json_data")(pybind11::none(), path);
if (pybind11::isinstance<pybind11::dict>(data))
@ -806,6 +806,13 @@ namespace O3DE::ProjectManager
templateInfo.m_canonicalTags.push_back(Py_To_String(tag));
}
}
if (data.contains("included_gems"))
{
for (auto gem : data["included_gems"])
{
templateInfo.m_includedGems.push_back(Py_To_String(gem));
}
}
}
catch ([[maybe_unused]] const std::exception& e)
{

@ -13,6 +13,7 @@
#if !defined(Q_MOC_RUN)
#include <ScreenDefs.h>
#include <ProjectInfo.h>
#include <QWidget>
#include <QStyleOption>
@ -61,6 +62,7 @@ namespace O3DE::ProjectManager
void GotoPreviousScreenRequest();
void ResetScreenRequest(ProjectManagerScreen screen);
void NotifyCurrentProject(const QString& projectPath);
void NotifyBuildProject(const ProjectInfo& projectInfo);
};

@ -177,6 +177,7 @@ namespace O3DE::ProjectManager
connect(newScreen, &ScreenWidget::GotoPreviousScreenRequest, this, &ScreensCtrl::GotoPreviousScreen);
connect(newScreen, &ScreenWidget::ResetScreenRequest, this, &ScreensCtrl::ResetScreen);
connect(newScreen, &ScreenWidget::NotifyCurrentProject, this, &ScreensCtrl::NotifyCurrentProject);
connect(newScreen, &ScreenWidget::NotifyBuildProject, this, &ScreensCtrl::NotifyBuildProject);
}
void ScreensCtrl::ResetAllScreens()

@ -13,6 +13,7 @@
#if !defined(Q_MOC_RUN)
#include <ScreenDefs.h>
#include <ProjectInfo.h>
#include <QStackedWidget>
#include <QStack>
@ -39,6 +40,7 @@ namespace O3DE::ProjectManager
signals:
void NotifyCurrentProject(const QString& projectPath);
void NotifyBuildProject(const ProjectInfo& projectInfo);
public slots:
bool ChangeToScreen(ProjectManagerScreen screen);

@ -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 <TemplateButtonWidget.h>
#include <QVBoxLayout>
#include <QLabel>
#include <QPixmap>
#include <QAbstractButton>
#include <QStyle>
#include <QVariant>
namespace O3DE::ProjectManager
{
TemplateButton::TemplateButton(const QString& imagePath, const QString& labelText, QWidget* parent)
: QPushButton(parent)
{
setAutoExclusive(true);
setObjectName("templateButton");
QVBoxLayout* vLayout = new QVBoxLayout();
vLayout->setSpacing(0);
vLayout->setContentsMargins(0, 0, 0, 0);
setLayout(vLayout);
QLabel* image = new QLabel(this);
image->setObjectName("templateImage");
image->setPixmap(
QPixmap(imagePath).scaled(QSize(s_templateImageWidth,s_templateImageHeight) , Qt::KeepAspectRatio, Qt::SmoothTransformation));
vLayout->addWidget(image);
QLabel* label = new QLabel(labelText, this);
label->setObjectName("templateLabel");
vLayout->addWidget(label);
connect(this, &QAbstractButton::toggled, this, &TemplateButton::onToggled);
}
void TemplateButton::onToggled()
{
setProperty("Checked", isChecked());
// we must unpolish/polish every child after changing a property
// or else they won't use the correct stylesheet selector
for (auto child : findChildren<QWidget*>())
{
child->style()->unpolish(child);
child->style()->polish(child);
}
style()->unpolish(this);
style()->polish(this);
}
} // namespace O3DE::ProjectManager

@ -0,0 +1,37 @@
/*
* 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
#if !defined(Q_MOC_RUN)
#include <QPushButton>
#endif
namespace O3DE::ProjectManager
{
class TemplateButton
: public QPushButton
{
Q_OBJECT // AUTOMOC
public:
explicit TemplateButton(const QString& imagePath, const QString& labelText, QWidget* parent = nullptr);
~TemplateButton() = default;
protected slots:
void onToggled();
private:
inline constexpr static int s_templateImageWidth = 92;
inline constexpr static int s_templateImageHeight = 122;
};
} // namespace O3DE::ProjectManager

@ -119,7 +119,9 @@ namespace O3DE::ProjectManager
void UpdateProjectCtrl::HandleNextButton()
{
if (m_stack->currentIndex() == ScreenOrder::Settings)
bool shouldRebuild = false;
if (m_stack->currentIndex() == ScreenOrder::Settings && m_updateSettingsScreen)
{
if (m_updateSettingsScreen)
{
@ -155,11 +157,17 @@ namespace O3DE::ProjectManager
m_projectInfo = newProjectSettings;
}
}
if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen)
else if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen)
{
// Enable or disable the gems that got adjusted in the gem catalog and apply them to the given project.
m_gemCatalogScreen->EnableDisableGemsForProject(m_projectInfo.m_path);
shouldRebuild = true;
}
if (shouldRebuild)
{
emit NotifyBuildProject(m_projectInfo);
}
emit ChangeScreenRequest(ProjectManagerScreen::Projects);

@ -38,6 +38,8 @@ set(FILES
Source/ProjectInfo.cpp
Source/ProjectUtils.h
Source/ProjectUtils.cpp
Source/ProjectBuilder.h
Source/ProjectBuilder.cpp
Source/UpdateProjectSettingsScreen.h
Source/UpdateProjectSettingsScreen.cpp
Source/NewProjectSettingsScreen.h
@ -60,6 +62,8 @@ set(FILES
Source/LinkWidget.cpp
Source/TagWidget.h
Source/TagWidget.cpp
Source/TemplateButtonWidget.h
Source/TemplateButtonWidget.cpp
Source/GemCatalog/GemCatalogHeaderWidget.h
Source/GemCatalog/GemCatalogHeaderWidget.cpp
Source/GemCatalog/GemCatalogScreen.h

@ -0,0 +1,22 @@
{
"materialType": "Materials\\Types\\StandardPBR.materialtype",
"propertyLayoutVersion": 3,
"properties": {
"general": {
"enableShadows": false,
"enableDirectionalLights": false,
"enablePunctualLights": false,
"enableAreaLights": false,
"enableIBL": true
},
"baseColor": {
"color": [ 0.0, 1.0, 0.0 ]
},
"opacity": {
"alphaSource": "None",
"doubleSided": true,
"factor": 0.25,
"mode": "TintedTransparent"
}
}
}

@ -0,0 +1,22 @@
{
"materialType": "Materials\\Types\\StandardPBR.materialtype",
"propertyLayoutVersion": 3,
"properties": {
"general": {
"enableShadows": false,
"enableDirectionalLights": false,
"enablePunctualLights": false,
"enableAreaLights": false,
"enableIBL": true
},
"baseColor": {
"color": [ 0.0, 1.0, 0.0 ]
},
"opacity": {
"alphaSource": "None",
"doubleSided": true,
"factor": 1.0,
"mode": "TintedTransparent"
}
}
}

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:75cdf73fcb9698a76a38294a1cf927a4fb41a34869e0429e1f02bf8d361a7258
size 20400

@ -0,0 +1,75 @@
{
"Type": "JsonSerialization",
"Version": 1,
"ClassName": "PassAsset",
"ClassData": {
"PassTemplate": {
"Name": "ContrastAdaptiveSharpeningTemplate",
"PassClass": "ComputePass",
"Slots": [
{
"Name": "InputColor",
"SlotType": "Input",
"ShaderInputName": "m_inputColor",
"ScopeAttachmentUsage": "Shader"
},
{
"Name": "OutputColor",
"SlotType": "Output",
"ShaderInputName": "m_outputColor",
"ScopeAttachmentUsage": "Shader"
}
],
"ImageAttachments": [
{
"Name": "Output",
"FormatSource": {
"Pass": "This",
"Attachment": "InputColor"
},
"SizeSource": {
"Source": {
"Pass": "This",
"Attachment": "InputColor"
}
},
"ImageDescriptor": {
"Format": "R16G16B16A16_FLOAT",
"BindFlags": "3",
"SharedQueueMask": "1"
}
}
],
"Connections": [
{
"LocalSlot": "OutputColor",
"AttachmentRef": {
"Pass": "This",
"Attachment": "Output"
}
}
],
"FallbackConnections": [
{
"Input": "InputColor",
"Output": "OutputColor"
}
],
"PassData": {
"$type": "ComputePassData",
"ShaderAsset": {
"FilePath": "Shaders/Postprocessing/ContrastAdaptiveSharpening.shader"
},
"Make Fullscreen Pass": true,
"ShaderDataMappings": {
"FloatMappings": [
{
"Name": "m_strength",
"Value": 0.25
}
]
}
}
}
}
}

@ -341,6 +341,13 @@
"Attachment": "Depth"
}
},
{
"LocalSlot": "MotionVectors",
"AttachmentRef": {
"Pass": "MotionVectorPass",
"Attachment": "MotionVectorOutput"
}
},
{
"LocalSlot": "SwapChainOutput",
"AttachmentRef": {

@ -13,22 +13,11 @@
"SlotType": "Input",
"ScopeAttachmentUsage": "InputAssembly"
},
// Outputs...
// Input/Output...
{
"Name": "Output",
"SlotType": "Output",
"ScopeAttachmentUsage": "RenderTarget",
"LoadStoreAction": {
"ClearValue": {
"Value": [
0.0,
0.0,
0.0,
{}
]
},
"LoadAction": "Clear"
}
"Name": "MotionInputOutput",
"SlotType": "InputOutput",
"ScopeAttachmentUsage": "RenderTarget"
},
{
"Name": "OutputDepthStencil",
@ -46,19 +35,6 @@
}
],
"ImageAttachments": [
{
"Name": "MotionBuffer",
"SizeSource": {
"Source": {
"Pass": "Parent",
"Attachment": "SwapChainOutput"
}
},
"ImageDescriptor": {
"Format": "R16G16_FLOAT",
"SharedQueueMask": "Graphics"
}
},
{
"Name": "DepthStencil",
"SizeSource": {
@ -74,13 +50,6 @@
}
],
"Connections": [
{
"LocalSlot": "Output",
"AttachmentRef": {
"Pass": "This",
"Attachment": "MotionBuffer"
}
},
{
"LocalSlot": "OutputDepthStencil",
"AttachmentRef": {

@ -20,6 +20,19 @@
{
"Name": "SwapChainOutput",
"SlotType": "InputOutput"
},
{
"Name": "MotionVectorOutput",
"SlotType": "Output"
}
],
"Connections": [
{
"LocalSlot": "MotionVectorOutput",
"AttachmentRef": {
"Pass": "MeshMotionVectorPass",
"Attachment": "MotionInputOutput"
}
}
],
"PassRequests": [
@ -50,6 +63,13 @@
"Pass": "Parent",
"Attachment": "SkinnedMeshes"
}
},
{
"LocalSlot": "MotionInputOutput",
"AttachmentRef": {
"Pass": "CameraMotionVectorPass",
"Attachment": "Output"
}
}
],
"PassData": {

@ -284,6 +284,14 @@
"Name": "SMAA1xApplyPerceptualColorTemplate",
"Path": "Passes/SMAA1xApplyPerceptualColor.pass"
},
{
"Name": "TaaTemplate",
"Path": "Passes/Taa.pass"
},
{
"Name": "ContrastAdaptiveSharpeningTemplate",
"Path": "Passes/ContrastAdaptiveSharpening.pass"
},
{
"Name": "SsaoParentTemplate",
"Path": "Passes/SsaoParent.pass"

@ -16,6 +16,10 @@
"Name": "Depth",
"SlotType": "Input"
},
{
"Name": "MotionVectors",
"SlotType": "Input"
},
// SwapChain here is only used to reference the frame height and format
{
"Name": "SwapChainOutput",
@ -40,8 +44,8 @@
{
"LocalSlot": "Output",
"AttachmentRef": {
"Pass": "LightAdaptation",
"Attachment": "Output"
"Pass": "ContrastAdaptiveSharpeningPass",
"Attachment": "OutputColor"
}
},
{
@ -80,6 +84,34 @@
}
]
},
{
"Name": "TaaPass",
"TemplateName": "TaaTemplate",
"Enabled": false,
"Connections": [
{
"LocalSlot": "InputColor",
"AttachmentRef": {
"Pass": "SMAA1xApplyLinearHDRColorPass",
"Attachment": "OutputColor"
}
},
{
"LocalSlot": "InputDepth",
"AttachmentRef": {
"Pass": "Parent",
"Attachment": "Depth"
}
},
{
"LocalSlot": "MotionVectors",
"AttachmentRef": {
"Pass": "Parent",
"Attachment": "MotionVectors"
}
}
]
},
{
"Name": "DepthOfFieldPass",
"TemplateName": "DepthOfFieldTemplate",
@ -88,7 +120,7 @@
{
"LocalSlot": "DoFColorInput",
"AttachmentRef": {
"Pass": "SMAA1xApplyLinearHDRColorPass",
"Pass": "TaaPass",
"Attachment": "OutputColor"
}
},
@ -134,6 +166,20 @@
}
}
]
},
{
"Name": "ContrastAdaptiveSharpeningPass",
"TemplateName": "ContrastAdaptiveSharpeningTemplate",
"Enabled": false,
"Connections": [
{
"LocalSlot": "InputColor",
"AttachmentRef": {
"Pass": "LightAdaptation",
"Attachment": "Output"
}
}
]
}
]
}

@ -40,6 +40,12 @@
}
}
],
"FallbackConnections": [
{
"Input": "InputColor",
"Output": "OutputColor"
}
],
"PassRequests": [
{
"Name": "SMAAConvertToPerceptualColor",

@ -0,0 +1,113 @@
{
"Type": "JsonSerialization",
"Version": 1,
"ClassName": "PassAsset",
"ClassData": {
"PassTemplate": {
"Name": "TaaTemplate",
"PassClass": "TaaPass",
"Slots": [
{
"Name": "InputColor",
"SlotType": "Input",
"ShaderInputName": "m_inputColor",
"ScopeAttachmentUsage": "Shader"
},
{
"Name": "InputDepth",
"SlotType": "Input",
"ShaderInputName": "m_inputDepth",
"ScopeAttachmentUsage": "Shader"
},
{
"Name": "MotionVectors",
"SlotType": "Input",
"ShaderInputName": "m_motionVectors",
"ScopeAttachmentUsage": "Shader"
},
{
"Name": "LastFrameAccumulation",
"SlotType": "Input",
"ShaderInputName": "m_lastFrameAccumulation",
"ScopeAttachmentUsage": "Shader"
},
{
"Name": "OutputColor",
"SlotType": "Output",
"ShaderInputName": "m_outputColor",
"ScopeAttachmentUsage": "Shader"
}
],
"ImageAttachments": [
{
"Name": "Accumulation1",
"Lifetime": "Imported",
"FormatSource": {
"Pass": "This",
"Attachment": "InputColor"
},
"SizeSource": {
"Source": {
"Pass": "This",
"Attachment": "InputColor"
}
},
"ImageDescriptor": {
"Format": "R16G16B16A16_FLOAT",
"BindFlags": "3",
"SharedQueueMask": "1"
}
},
{
"Name": "Accumulation2",
"Lifetime": "Imported",
"FormatSource": {
"Pass": "This",
"Attachment": "InputColor"
},
"SizeSource": {
"Source": {
"Pass": "This",
"Attachment": "InputColor"
}
},
"ImageDescriptor": {
"Format": "R16G16B16A16_FLOAT",
"BindFlags": "3",
"SharedQueueMask": "1"
}
}
],
"FallbackConnections": [
{
"Input": "InputColor",
"Output": "OutputColor"
}
],
"PassData": {
"$type": "TaaPassData",
"ShaderAsset": {
"FilePath": "Shaders/Postprocessing/Taa.shader"
},
"Make Fullscreen Pass": true,
"ShaderDataMappings": {
"FloatMappings": [
{
"Name": "m_currentFrameContribution",
"Value": 0.1
},
{
"Name": "m_clampGamma",
"Value": 1.0
},
{
"Name": "m_maxDeviationBeforeDampening",
"Value": 0.5
}
]
},
"NumJitterPositions": 16
}
}
}
}

@ -37,6 +37,9 @@ ShaderResourceGroup PassSrg : SRG_PerPass
AddressV = Clamp;
AddressW = Clamp;
};
// scale multiplier of the downsampled size to the fullscreen size (e.g., 4)
uint m_imageScale;
}
#include <Atom/RPI/ShaderResourceGroups/DefaultDrawSrg.azsli>
@ -148,13 +151,10 @@ float3 SampleGlobalIBL(uint sampleIndex, uint2 screenCoords, float depth, float3
PSOutput MainPS(VSOutput IN, in uint sampleIndex : SV_SampleIndex)
{
uint2 screenCoords = IN.m_position.xy;
// [GFX TODO][ATOM-6172] Add image scale PassSrg constant to the DiffuseProbeGrid downsample/upsample
const uint ImageScale = 4;
const float ImageScaleInverse = 1.0f / ImageScale;
float imageScaleInverse = 1.0f / PassSrg::m_imageScale;
// compute image coords for the downsampled probe irradiance image
uint2 probeIrradianceCoords = screenCoords * ImageScaleInverse;
uint2 probeIrradianceCoords = screenCoords * imageScaleInverse;
float depth = PassSrg::m_depth.Load(screenCoords, sampleIndex).r;
float4 encodedNormal = PassSrg::m_normal.Load(screenCoords, sampleIndex);
@ -165,7 +165,7 @@ PSOutput MainPS(VSOutput IN, in uint sampleIndex : SV_SampleIndex)
float3 diffuse = float3(0.0f, 0.0f, 0.0f);
if (useProbeIrradiance > 0.0f)
{
float3 irradiance = SampleProbeIrradiance(sampleIndex, probeIrradianceCoords, depth, normal, albedo, ImageScale);
float3 irradiance = SampleProbeIrradiance(sampleIndex, probeIrradianceCoords, depth, normal, albedo, PassSrg::m_imageScale);
diffuse = (albedo.rgb / PI) * irradiance;
}
else

@ -31,6 +31,9 @@ ShaderResourceGroup PassSrg : SRG_PerPass
AddressV = Clamp;
AddressW = Clamp;
};
// scale multiplier of the downsampled size to the fullscreen size (e.g., 4)
uint m_outputImageScale;
}
#include <Atom/RPI/ShaderResourceGroups/DefaultDrawSrg.azsli>
@ -56,16 +59,13 @@ struct PSOutput
// Pixel Shader
PSOutput MainPS(VSOutput IN, in uint sampleIndex : SV_SampleIndex)
{
// the downsample is 1/4 resolution
// [GFX TODO][ATOM-6172] Add image scale PassSrg constant to the DiffuseProbeGrid downsample/upsample
const uint ImageScale = 4;
uint2 screenCoords = IN.m_position.xy * ImageScale;
uint2 screenCoords = IN.m_position.xy * PassSrg::m_outputImageScale;
float downsampledDepth = 0;
float4 downsampledEncodedNormal;
for (uint y = 0; y < ImageScale; ++y)
for (uint y = 0; y < PassSrg::m_outputImageScale; ++y)
{
for (uint x = 0; x < ImageScale; ++x)
for (uint x = 0; x < PassSrg::m_outputImageScale; ++x)
{
float depth = PassSrg::m_depth.Load(screenCoords + int2(x, y), sampleIndex).r;
float4 encodedNormal = PassSrg::m_normal.Load(screenCoords + int2(x, y), sampleIndex);

@ -39,10 +39,27 @@ PSOutput MainPS(VSOutput IN)
PSOutput OUT;
float depth = PassSrg::m_depthStencil.Sample(PassSrg::LinearSampler, IN.m_texCoord).r;
// If depth is 0, that means depth is on the far plane. This should be treated as being infinitely far
// away, not actually on the far plane, because the infinitely far background shouldn't move as a result
// of camera translation. Tweaking the depth to -near/far distance makes that happen. Keep in mind near
// and far are inverted, so this normally a very small value.
if (depth == 0.0)
{
depth = -ViewSrg::GetFarZ() / ViewSrg::GetNearZ();
}
float2 clipPos = float2(mad(IN.m_texCoord.x, 2.0, -1.0), mad(IN.m_texCoord.y, -2.0, 1.0));
float4 worldPos = mul(ViewSrg::m_viewProjectionInverseMatrix, float4(clipPos, depth, 1.0));
float4 clipPosPrev = mul(ViewSrg::m_viewProjectionPrevMatrix, float4((worldPos / worldPos.w).xyz, 1.0));
OUT.m_motion = (clipPos - (clipPosPrev / clipPosPrev.w).xy) * 0.5;
clipPosPrev = (clipPosPrev / clipPosPrev.w);
// Clip space is from -1.0 to 1.0, so the motion vectors are 2x as big as they should be
OUT.m_motion = (clipPos - clipPosPrev.xy) * 0.5;
// Flip y to line up with uv coordinates
OUT.m_motion.y = -OUT.m_motion.y;
return OUT;
}

@ -41,5 +41,9 @@ PSOutput MainPS(VSOutput IN)
float2 motion = (clipPos.xy / clipPos.w - clipPosPrev.xy / clipPosPrev.w) * 0.5;
OUT.m_motion = motion;
// Flip y to line up with uv coordinates
OUT.m_motion.y = -OUT.m_motion.y;
return OUT;
}

@ -0,0 +1,85 @@
/*
* 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 <scenesrg.srgi>
#include <Atom/Features/PostProcessing/PostProcessUtil.azsli>
#define TILE_DIM_X 16
#define TILE_DIM_Y 16
ShaderResourceGroup PassSrg : SRG_PerPass
{
Texture2D<float4> m_inputColor;
RWTexture2D<float4> m_outputColor;
float m_strength; // Strength of the sharpening effect. Range from 0 to 1.
}
// Constrast Adaptive Sharpening, based on AMD FidelityFX CAS - https://gpuopen.com/fidelityfx-cas/
// This shader sharpens the input based on the contrast of the local neighborhood
// so that only areas that need sharpening are sharpened, while high constast areas
// are mostly left alone.
[numthreads(TILE_DIM_X, TILE_DIM_Y, 1)]
void MainCS(
uint3 dispatchThreadID : SV_DispatchThreadID,
uint3 groupID : SV_GroupID,
uint groupIndex : SV_GroupIndex)
{
uint2 pixelCoord = dispatchThreadID.xy;
// Fetch local neighborhood to determin sharpening weight.
// a
// b c d
// e
float3 sampleA = PassSrg::m_inputColor[pixelCoord + int2( 0, -1)].rgb;
float3 sampleB = PassSrg::m_inputColor[pixelCoord + int2(-1, 0)].rgb;
float3 sampleC = PassSrg::m_inputColor[pixelCoord + int2( 0, 0)].rgb;
float3 sampleD = PassSrg::m_inputColor[pixelCoord + int2( 1, 0)].rgb;
float3 sampleE = PassSrg::m_inputColor[pixelCoord + int2( 0, 1)].rgb;
float lumA = GetLuminance(sampleA);
float lumB = GetLuminance(sampleB);
float lumC = GetLuminance(sampleC);
float lumD = GetLuminance(sampleD);
float lumE = GetLuminance(sampleE);
// Get the min and max. Just use the green channel for luminance.
float minLum = min(min(lumA, lumB), min(lumC, min(lumD, lumE)));
float maxLum = max(max(lumA, lumB), max(lumC, max(lumD, lumE)));
float dMinLum = minLum; // Distance from 0 to minimum
float dMaxLum = 1.0 - maxLum; // Distance from 1 to the maximum
// baseSharpening is higher when local contrast is lower to avoid over-sharpening.
float baseSharpening = min(dMinLum, dMaxLum) / max(maxLum, 0.0001);
baseSharpening = sqrt(baseSharpening); // bias towards more sharpening
// Negative weights for sharpening effect, center pixel is always weighted 1.
float developerMaximum = lerp(-0.125, -0.2, PassSrg::m_strength);
float weight = baseSharpening * developerMaximum;
float totalWeight = weight * 4 + 1.0;
float3 output =
(
sampleA * weight +
sampleB * weight +
sampleC +
sampleD * weight +
sampleE * weight
) / totalWeight;
PassSrg::m_outputColor[pixelCoord] = float4(output, 1.0);
}

@ -0,0 +1,11 @@
{
"Source": "ContrastAdaptiveSharpening",
"ProgramSettings": {
"EntryPoints": [
{
"name": "MainCS",
"type": "Compute"
}
]
}
}

@ -0,0 +1,271 @@
/*
* 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 <scenesrg.srgi>
#include <Atom/Features/PostProcessing/PostProcessUtil.azsli>
#define TILE_DIM_X 16
#define TILE_DIM_Y 16
ShaderResourceGroup PassSrg : SRG_PerPass
{
Texture2D<float4> m_inputColor;
Texture2D<float4> m_inputDepth;
Texture2D<float2> m_motionVectors;
Texture2D<float4> m_lastFrameAccumulation;
RWTexture2D<float4> m_outputColor;
Sampler LinearSampler
{
MinFilter = Linear;
MagFilter = Linear;
MipFilter = Linear;
AddressU = Clamp;
AddressV = Clamp;
AddressW = Clamp;
};
// Current frame's default contribution to the history.
float m_currentFrameContribution;
// Increase this value for weaker clamping, decrease for stronger clamping, default 1.0.
float m_clampGamma;
// Default 0.5, used for flicker reduction. Any sample further than this many standard deviations outside the neighborhood
// will have its weight decreased. The further outside the max deviation, the more its weight is reduced.
float m_maxDeviationBeforeDampening;
struct Constants
{
uint2 m_inputColorSize;
float2 m_inputColorRcpSize;
// 3x3 filter weights
// 8 2 6
// 3 0 1
// 7 4 5
float4 m_weights1; // 0 1 2 3
float4 m_weights2; // 4 5 6 7
float4 m_weights3; // 8 x x x
};
Constants m_constantData;
}
static const int2 offsets[9] =
{
// Center
int2(0, 0),
// Cross
int2( 1, 0),
int2( 0,-1),
int2(-1, 0),
int2( 0, 1),
// Diagonals
int2( 1,-1),
int2( 1, 1),
int2(-1,-1),
int2(-1, 1),
};
float3 RgbToYCoCg(float3 rgb)
{
const float3x3 conversionMatrix =
{
0.25, 0.50, 0.25,
0.50, 0.00, -0.50,
-0.25, 0.50, -0.25
};
return mul(conversionMatrix, rgb);
}
float3 YCoCgToRgb(float3 yCoCg)
{
const float3x3 conversionMatrix =
{
1.0, 1.0, -1.0,
1.0, 0.0, 1.0,
1.0, -1.0, -1.0
};
return mul(conversionMatrix, yCoCg);
}
// Sample a texture with a 5 tap Catmull-Rom. Consider ripping this out and putting in a more general location.
// This function samples a 4x4 neighborhood around the uv. By taking advantage of bilinear filtering this can be
// done with only 9 taps on the edges between pixels. The cost is further reduced by dropping the 4 diagonal
// samples as their influence is negligible.
float4 SampleCatmullRom5Tap(Texture2D<float4> texture, SamplerState linearSampler, float2 uv, float2 textureSize, float2 rcpTextureSize, float sharpness)
{
// Think of sample locations in the 4x4 neighborhood as having a top left coordinate of 0,0 and
// a bottom right coordinate of 3,3.
// Find the position in texture space then round it to get the center of the 1,1 pixel (tc1)
float2 texelPos = uv * textureSize;
float2 tc1= floor(texelPos - 0.5) + 0.5;
// Offset from center position to texel
float2 f = texelPos - tc1;
// Compute Catmull-Rom weights based on the offset and sharpness
float c = sharpness;
float2 w0 = f * (-c + f * (2.0 * c - c * f));
float2 w1 = 1.0 + f * f * (c -3.0 + (2.0 - c) * f);
float2 w2 = f * (c + f * ((3.0 - 2.0 * c) - (2.0 - c) * f));
float2 w3 = f * f * (c * f - c);
float2 w12 = w1 + w2;
// Compute uv coordinates for sampling the texture
float2 tc0 = (tc1 - 1.0f) * rcpTextureSize;
float2 tc3 = (tc1 + 2.0f) * rcpTextureSize;
float2 tc12 = (tc1 + w2 / w12) * rcpTextureSize;
// Compute sample weights
float sw0 = w12.x * w0.y;
float sw1 = w0.x * w12.y;
float sw2 = w12.x * w12.y;
float sw3 = w3.x * w12.y;
float sw4 = w12.x * w3.y;
// total weight of samples to normalize result.
float totalWeight = sw0 + sw1 + sw2 + sw3 + sw4;
float4 result = 0.0f;
result += texture.SampleLevel(linearSampler, float2(tc12.x, tc0.y), 0.0) * sw0;
result += texture.SampleLevel(linearSampler, float2( tc0.x, tc12.y), 0.0) * sw1;
result += texture.SampleLevel(linearSampler, float2(tc12.x, tc12.y), 0.0) * sw2;
result += texture.SampleLevel(linearSampler, float2( tc3.x, tc12.y), 0.0) * sw3;
result += texture.SampleLevel(linearSampler, float2(tc12.x, tc3.y), 0.0) * sw4;
return result / totalWeight;
}
[numthreads(TILE_DIM_X, TILE_DIM_Y, 1)]
void MainCS(
uint3 dispatchThreadID : SV_DispatchThreadID,
uint3 groupID : SV_GroupID,
uint groupIndex : SV_GroupIndex)
{
uint2 pixelCoord = dispatchThreadID.xy;
const float filterWeights[9] =
{
PassSrg::m_constantData.m_weights1.x,
PassSrg::m_constantData.m_weights1.y,
PassSrg::m_constantData.m_weights1.z,
PassSrg::m_constantData.m_weights1.w,
PassSrg::m_constantData.m_weights2.x,
PassSrg::m_constantData.m_weights2.y,
PassSrg::m_constantData.m_weights2.z,
PassSrg::m_constantData.m_weights2.w,
PassSrg::m_constantData.m_weights3.x,
};
float3 sum = 0.0;
float3 sumOfSquares = 0.0;
float nearestDepth = 1.0;
uint2 nearestDepthPixelCoord;
float3 thisFrameColor = float3(0.0, 0.0, 0.0);
// Sample the neighborhood to filter the current pixel, gather statistics about
// its neighbors, and find the closest neighbor to choose a motion vector.
[unroll] for (int i = 0; i < 9; ++i)
{
uint2 neighborhoodPixelCoord = pixelCoord + offsets[i];
float3 neighborhoodColor = PassSrg::m_inputColor[neighborhoodPixelCoord].rgb;
// Convert to YCoCg space for better clipping.
neighborhoodColor = RgbToYCoCg(neighborhoodColor);
sum += neighborhoodColor;
sumOfSquares += neighborhoodColor * neighborhoodColor;
thisFrameColor += neighborhoodColor * filterWeights[i];
// Find the coordinate of the nearest depth
float neighborhoodDepth = PassSrg::m_inputDepth[neighborhoodPixelCoord].r;
if (neighborhoodDepth < nearestDepth)
{
nearestDepth = neighborhoodDepth;
nearestDepthPixelCoord = neighborhoodPixelCoord;
}
}
// Variance clipping, see http://developer.download.nvidia.com/gameworks/events/GDC2016/msalvi_temporal_supersampling.pdf
float3 mean = sum / 9.0;
float3 standardDeviation = max(0.0, sqrt(sumOfSquares / 9.0 - mean * mean));
standardDeviation *= PassSrg::m_clampGamma;
// Grab the motion vector from the closest pixel in the 3x3 neighborhood. This is done so that motion vectors correctly
// track edges. For instance, if a pixel lies on the edge of a moving object, where the color is a blend of the
// forground and background, it's possible for the pixel center to hit the (not moving) background. However, the correct
// history for this pixel will be the location this edge was the previous frame. By choosing the motion of the nearest
// pixel in the neighborhood that edge will be correctly tracked.
// Motion vectors store the direction of movement, so to look up where things were in the previous frame, it's negated.
float2 previousPositionOffset = -PassSrg::m_motionVectors[nearestDepthPixelCoord];
// Get the uv coordinate for the previous frame.
float2 rcpSize = PassSrg::m_constantData.m_inputColorRcpSize;
float2 uvCoord = (pixelCoord + 0.5f) * rcpSize;
float2 uvOld = uvCoord + previousPositionOffset;
float2 previousPositionOffsetInPixels = float2(PassSrg::m_constantData.m_inputColorSize) * previousPositionOffset;
// Sample the last frame using a 5-tap Catmull-Rom
float3 lastFrameColor = SampleCatmullRom5Tap(PassSrg::m_lastFrameAccumulation, PassSrg::LinearSampler, uvOld, PassSrg::m_constantData.m_inputColorSize, PassSrg::m_constantData.m_inputColorRcpSize, 0.5).rgb;
lastFrameColor = RgbToYCoCg(lastFrameColor);
// Last frame color relative to mean
float3 centerColorOffset = lastFrameColor - mean;
float3 colorOffsetStandardDeviationRatio = abs(standardDeviation / centerColorOffset);
// Clamp the color by the aabb of the standardDeviation. Can never be greater than 1, so will always be inside or on the bounds of the aabb.
float clampedColorLength = min(min(min(1, colorOffsetStandardDeviationRatio.x), colorOffsetStandardDeviationRatio.y), colorOffsetStandardDeviationRatio.z);
// Calculate the true clamped color by offsetting it back from the mean.
float3 lastFrameClampedColor = mean + centerColorOffset * clampedColorLength;
// Anti-flickering - Reduce current frame weight the more it deviates from the history based on the standard deviation of the neighborhood.
// Start reducing weight at differences greater than m_maxDeviationBeforeDampening standard deviations in luminance.
float standardDeviationWeight = standardDeviation.r * PassSrg::m_maxDeviationBeforeDampening;
float3 sdFromLastFrame = standardDeviationWeight / abs(lastFrameClampedColor.r - thisFrameColor.r);
float currentFrameWeight = PassSrg::m_currentFrameContribution;
currentFrameWeight *= saturate(sdFromLastFrame * sdFromLastFrame);
// Back to Rgb space
thisFrameColor = YCoCgToRgb(thisFrameColor);
lastFrameClampedColor = YCoCgToRgb(lastFrameClampedColor);
// Out of bounds protection.
if (any(uvOld > 1.0) || any(uvOld < 0.0))
{
currentFrameWeight = 1.0f;
}
// Blend should be in perceptual space, so tonemap first
float luminance = GetLuminance(thisFrameColor);
thisFrameColor = thisFrameColor / (1 + luminance);
lastFrameClampedColor = lastFrameClampedColor / (1 + luminance);
// Blend color with history
float3 color = lerp(lastFrameClampedColor, thisFrameColor, currentFrameWeight);
// Un-tonemap color
color = color * (1.0 + luminance);
// NaN protection (without this NaNs could get in the history buffer and quickly consume the frame)
color = max(0.0, color);
PassSrg::m_outputColor[pixelCoord].rgb = color;
}

@ -0,0 +1,11 @@
{
"Source": "Taa",
"ProgramSettings": {
"EntryPoints": [
{
"name": "MainCS",
"type": "Compute"
}
]
}
}

@ -89,6 +89,7 @@ set(FILES
Passes/CascadedShadowmaps.pass
Passes/CheckerboardResolveColor.pass
Passes/CheckerboardResolveDepth.pass
Passes/ContrastAdaptiveSharpening.pass
Passes/ConvertToAcescg.pass
Passes/DebugOverlayParent.pass
Passes/DeferredFog.pass
@ -207,6 +208,7 @@ set(FILES
Passes/SsaoHalfRes.pass
Passes/SsaoParent.pass
Passes/SubsurfaceScattering.pass
Passes/Taa.pass
Passes/Transparent.pass
Passes/TransparentParent.pass
Passes/UI.pass

@ -177,4 +177,5 @@ namespace AZ
} // namespace Render
AZ_TYPE_INFO_SPECIALIZE(Render::DisplayMapperOperationType, "{41CA80B1-9E0D-41FB-A235-9638D2A905A5}");
AZ_TYPE_INFO_SPECIALIZE(Render::OutputDeviceTransformType, "{B94085B7-C0D4-466A-A791-188A4559EC8D}");
} // namespace AZ

@ -0,0 +1,40 @@
/*
* 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/base.h>
#include <Atom/RPI.Public/FeatureProcessor.h>
namespace AZ
{
namespace Render
{
enum class DiffuseGlobalIlluminationQualityLevel : uint8_t
{
Low,
Medium,
High
};
//! This class provides general features and configuration for the diffuse global illumination environment,
//! which consists of DiffuseProbeGrids and the diffuse Global IBL cubemap.
class DiffuseGlobalIlluminationFeatureProcessorInterface
: public RPI::FeatureProcessor
{
public:
AZ_RTTI(AZ::Render::DiffuseProbeGridFeatureProcessorInterface, "{BD8CA35A-47C3-4FD8-932B-18495EF07527}");
virtual void SetQualityLevel(DiffuseGlobalIlluminationQualityLevel qualityLevel) = 0;
};
} // namespace Render
} // namespace AZ

@ -12,11 +12,13 @@
#pragma once
#include <AzCore/Asset/AssetCommon.h>
#include <ACES/Aces.h>
#include <Atom/RPI.Reflect/Pass/PassAsset.h>
#include <Atom/RPI.Reflect/Pass/PassData.h>
#include <Atom/RPI.Reflect/System/AnyAsset.h>
#include <AzCore/Asset/AssetCommon.h>
namespace AZ
{
@ -33,6 +35,7 @@ namespace AZ
AZ_TYPE_INFO(AcesParameterOverrides, "{3EE8C0D4-3792-46C0-B91C-B89A81C36B91}");
static void Reflect(ReflectContext* context);
// Load preconfigured preset for specific ODT mode defined by m_preset
void LoadPreset();
// When enabled allows parameter overrides for ACES configuration
@ -98,6 +101,5 @@ namespace AZ
DisplayMapperConfigurationDescriptor m_config;
};
} // namespace RPI
} // namespace Render
} // namespace AZ

@ -0,0 +1,43 @@
/*
* 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/base.h>
#include <AzCore/Math/Transform.h>
#include <Atom/RPI.Public/FeatureProcessor.h>
namespace AZ
{
namespace Render
{
class OcclusionCullingPlane;
using OcclusionCullingPlaneHandle = AZStd::shared_ptr<OcclusionCullingPlane>;
// OcclusionCullingPlaneFeatureProcessorInterface provides an interface to the feature processor for code outside of Atom
class OcclusionCullingPlaneFeatureProcessorInterface
: public RPI::FeatureProcessor
{
public:
AZ_RTTI(AZ::Render::OcclusionCullingPlaneFeatureProcessorInterface, "{50F6B45E-A622-44EC-B962-DE25FBD44095}");
virtual OcclusionCullingPlaneHandle AddOcclusionCullingPlane(const AZ::Transform& transform) = 0;
virtual void RemoveOcclusionCullingPlane(OcclusionCullingPlaneHandle& handle) = 0;
virtual bool IsValidOcclusionCullingPlaneHandle(const OcclusionCullingPlaneHandle& occlusionCullingPlane) const = 0;
virtual void SetTransform(const OcclusionCullingPlaneHandle& occlusionCullingPlane, const AZ::Transform& transform) = 0;
virtual void SetEnabled(const OcclusionCullingPlaneHandle& occlusionCullingPlane, bool enabled) = 0;
virtual void ShowVisualization(const OcclusionCullingPlaneHandle& occlusionCullingPlane, bool showVisualization) = 0;
virtual void SetTransparentVisualization(const OcclusionCullingPlaneHandle& occlusionCullingPlane, bool transparentVisualization) = 0;
};
} // namespace Render
} // namespace AZ

@ -64,6 +64,7 @@
#include <PostProcessing/SMAANeighborhoodBlendingPass.h>
#include <PostProcessing/SsaoPasses.h>
#include <PostProcessing/SubsurfaceScatteringPass.h>
#include <PostProcessing/TaaPass.h>
#include <PostProcessing/BloomDownsamplePass.h>
#include <PostProcessing/BloomBlurPass.h>
#include <PostProcessing/BloomCompositePass.h>
@ -91,17 +92,19 @@
#include <RayTracing/RayTracingAccelerationStructurePass.h>
#include <RayTracing/RayTracingPass.h>
#include <RayTracing/RayTracingPassData.h>
#include <DiffuseProbeGrid/DiffuseProbeGridRayTracingPass.h>
#include <DiffuseProbeGrid/DiffuseProbeGridBlendIrradiancePass.h>
#include <DiffuseProbeGrid/DiffuseProbeGridBlendDistancePass.h>
#include <DiffuseProbeGrid/DiffuseProbeGridBorderUpdatePass.h>
#include <DiffuseProbeGrid/DiffuseProbeGridRelocationPass.h>
#include <DiffuseProbeGrid/DiffuseProbeGridClassificationPass.h>
#include <DiffuseProbeGrid/DiffuseProbeGridRenderPass.h>
#include <DiffuseProbeGrid/DiffuseProbeGridFeatureProcessor.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridRayTracingPass.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridBlendIrradiancePass.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridBlendDistancePass.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridBorderUpdatePass.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridRelocationPass.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridClassificationPass.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridRenderPass.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridFeatureProcessor.h>
#include <DiffuseGlobalIllumination/DiffuseGlobalIlluminationFeatureProcessor.h>
#include <ReflectionScreenSpace/ReflectionScreenSpaceBlurPass.h>
#include <ReflectionScreenSpace/ReflectionScreenSpaceBlurChildPass.h>
#include <ReflectionScreenSpace/ReflectionCopyFrameBufferPass.h>
#include <OcclusionCullingPlane/OcclusionCullingPlaneFeatureProcessor.h>
namespace AZ
{
@ -131,11 +134,14 @@ namespace AZ
PostProcessFeatureProcessor::Reflect(context);
ImGuiPassData::Reflect(context);
RayTracingPassData::Reflect(context);
TaaPassData::Reflect(context);
LightingPreset::Reflect(context);
ModelPreset::Reflect(context);
DiffuseProbeGridFeatureProcessor::Reflect(context);
DiffuseGlobalIlluminationFeatureProcessor::Reflect(context);
RayTracingFeatureProcessor::Reflect(context);
OcclusionCullingPlaneFeatureProcessor::Reflect(context);
if (SerializeContext* serialize = azrtti_cast<SerializeContext*>(context))
{
@ -191,7 +197,9 @@ namespace AZ
AZ::RPI::FeatureProcessorFactory::Get()->RegisterFeatureProcessor<ReflectionProbeFeatureProcessor>();
AZ::RPI::FeatureProcessorFactory::Get()->RegisterFeatureProcessor<SMAAFeatureProcessor>();
AZ::RPI::FeatureProcessorFactory::Get()->RegisterFeatureProcessor<DiffuseProbeGridFeatureProcessor>();
AZ::RPI::FeatureProcessorFactory::Get()->RegisterFeatureProcessor<DiffuseGlobalIlluminationFeatureProcessor>();
AZ::RPI::FeatureProcessorFactory::Get()->RegisterFeatureProcessor<RayTracingFeatureProcessor>();
AZ::RPI::FeatureProcessorFactory::Get()->RegisterFeatureProcessor<OcclusionCullingPlaneFeatureProcessor>();
// Add SkyBox pass
auto* passSystem = RPI::PassSystemInterface::Get();
@ -224,6 +232,9 @@ namespace AZ
// Add Depth Downsample/Upsample passes
passSystem->AddPassCreator(Name("DepthUpsamplePass"), &DepthUpsamplePass::Create);
// Add Taa Pass
passSystem->AddPassCreator(Name("TaaPass"), &TaaPass::Create);
// Add DepthOfField pass
passSystem->AddPassCreator(Name("DepthOfFieldCompositePass"), &DepthOfFieldCompositePass::Create);
@ -285,6 +296,7 @@ namespace AZ
void CommonSystemComponent::Deactivate()
{
AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor<RayTracingFeatureProcessor>();
AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor<DiffuseGlobalIlluminationFeatureProcessor>();
AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor<DiffuseProbeGridFeatureProcessor>();
AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor<SMAAFeatureProcessor>();
AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor<ReflectionProbeFeatureProcessor>();
@ -297,6 +309,7 @@ namespace AZ
AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor<SkyBoxFeatureProcessor>();
AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor<TransformServiceFeatureProcessor>();
AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor<AuxGeomFeatureProcessor>();
AZ::RPI::FeatureProcessorFactory::Get()->UnregisterFeatureProcessor<OcclusionCullingPlaneFeatureProcessor>();
}
void CommonSystemComponent::LoadPassTemplateMappings()

@ -311,14 +311,9 @@ namespace AZ
{
auto tileBufferResolution = GetTileDataBufferResolution();
// generate a UUID for the buffer name to keep it unique when there are multiple render pipelines
AZ::Uuid uuid = AZ::Uuid::CreateRandom();
AZStd::string uuidString;
uuid.ToString(uuidString);
RPI::CommonBufferDescriptor desc;
desc.m_poolType = RPI::CommonBufferPoolType::ReadWrite;
desc.m_bufferName = AZStd::string::format("LightList_%s", uuidString.c_str());
desc.m_bufferName = "LightList";
desc.m_elementSize = sizeof(uint32_t);
desc.m_byteCount = tileBufferResolution.m_width * tileBufferResolution.m_height * 256 * sizeof(uint32_t);
m_lightList = RPI::BufferSystemInterface::Get()->CreateBufferFromCommonPool(desc);

@ -118,14 +118,9 @@ namespace AZ
void LightCullingRemap::CreateRemappedLightListBuffer()
{
// generate a UUID for the buffer name to keep it unique when there are multiple render pipelines
AZ::Uuid uuid = AZ::Uuid::CreateRandom();
AZStd::string uuidString;
uuid.ToString(uuidString);
RPI::CommonBufferDescriptor desc;
desc.m_poolType = RPI::CommonBufferPoolType::ReadWrite;
desc.m_bufferName = AZStd::string::format("LightListRemapped_%s", uuidString.c_str());
desc.m_bufferName = "LightListRemapped";
desc.m_elementSize = RHI::GetFormatSize(LightListRemappedFormat);
desc.m_byteCount = m_tileDim.m_width * m_tileDim.m_height * NumBins * MaxLightsPerTile * desc.m_elementSize;
m_lightListRemapped = RPI::BufferSystemInterface::Get()->CreateBufferFromCommonPool(desc);

@ -0,0 +1,113 @@
/*
* 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 <Atom/RPI.Public/RPIUtils.h>
#include <Atom/RPI.Public/Scene.h>
#include <Atom/RPI.Public/Pass/PassFilter.h>
#include <Atom/RPI.Public/Pass/FullscreenTrianglePass.h>
#include <DiffuseGlobalIllumination/DiffuseGlobalIlluminationFeatureProcessor.h>
namespace AZ
{
namespace Render
{
void DiffuseGlobalIlluminationFeatureProcessor::Reflect(ReflectContext* context)
{
if (auto* serializeContext = azrtti_cast<SerializeContext*>(context))
{
serializeContext
->Class<DiffuseGlobalIlluminationFeatureProcessor, FeatureProcessor>()
->Version(0);
}
}
void DiffuseGlobalIlluminationFeatureProcessor::Activate()
{
EnableSceneNotification();
}
void DiffuseGlobalIlluminationFeatureProcessor::Deactivate()
{
DisableSceneNotification();
}
void DiffuseGlobalIlluminationFeatureProcessor::SetQualityLevel(DiffuseGlobalIlluminationQualityLevel qualityLevel)
{
m_qualityLevel = qualityLevel;
UpdatePasses();
}
void DiffuseGlobalIlluminationFeatureProcessor::OnRenderPipelinePassesChanged([[maybe_unused]] RPI::RenderPipeline* renderPipeline)
{
UpdatePasses();
}
void DiffuseGlobalIlluminationFeatureProcessor::OnRenderPipelineAdded([[maybe_unused]] RPI::RenderPipelinePtr pipeline)
{
UpdatePasses();
}
void DiffuseGlobalIlluminationFeatureProcessor::UpdatePasses()
{
float sizeMultiplier = 0.0f;
switch (m_qualityLevel)
{
case DiffuseGlobalIlluminationQualityLevel::Low:
sizeMultiplier = 0.25f;
break;
case DiffuseGlobalIlluminationQualityLevel::Medium:
sizeMultiplier = 0.5f;
break;
case DiffuseGlobalIlluminationQualityLevel::High:
sizeMultiplier = 1.0f;
break;
default:
AZ_Assert(false, "Unknown DiffuseGlobalIlluminationQualityLevel [%d]", m_qualityLevel);
break;
}
// update the size multiplier on the DiffuseProbeGridDownsamplePass output
AZStd::vector<Name> downsamplePassHierarchy = { Name("DiffuseGlobalIlluminationPass"), Name("DiffuseProbeGridDownsamplePass") };
RPI::PassHierarchyFilter downsamplePassFilter(downsamplePassHierarchy);
const AZStd::vector<RPI::Pass*>& downsamplePasses = RPI::PassSystemInterface::Get()->FindPasses(downsamplePassFilter);
for (RPI::Pass* pass : downsamplePasses)
{
for (uint32_t outputIndex = 0; outputIndex < pass->GetOutputCount(); ++outputIndex)
{
RPI::Ptr<RPI::PassAttachment> outputAttachment = pass->GetOutputBinding(outputIndex).m_attachment;
RPI::PassAttachmentSizeMultipliers& sizeMultipliers = outputAttachment->m_sizeMultipliers;
sizeMultipliers.m_widthMultiplier = sizeMultiplier;
sizeMultipliers.m_heightMultiplier = sizeMultiplier;
}
// set the output scale on the PassSrg
RPI::FullscreenTrianglePass* downsamplePass = static_cast<RPI::FullscreenTrianglePass*>(pass);
auto constantIndex = downsamplePass->GetShaderResourceGroup()->FindShaderInputConstantIndex(Name("m_outputImageScale"));
downsamplePass->GetShaderResourceGroup()->SetConstant(constantIndex, aznumeric_cast<uint32_t>(1.0f / sizeMultiplier));
}
// update the image scale on the DiffuseComposite pass
AZStd::vector<Name> compositePassHierarchy = { Name("DiffuseGlobalIlluminationPass"), Name("DiffuseCompositePass") };
RPI::PassHierarchyFilter compositePassFilter(compositePassHierarchy);
const AZStd::vector<RPI::Pass*>& compositePasses = RPI::PassSystemInterface::Get()->FindPasses(compositePassFilter);
for (RPI::Pass* pass : compositePasses)
{
RPI::FullscreenTrianglePass* compositePass = static_cast<RPI::FullscreenTrianglePass*>(pass);
auto constantIndex = compositePass->GetShaderResourceGroup()->FindShaderInputConstantIndex(Name("m_imageScale"));
compositePass->GetShaderResourceGroup()->SetConstant(constantIndex, aznumeric_cast<uint32_t>(1.0f / sizeMultiplier));
}
}
} // namespace Render
} // namespace AZ

@ -0,0 +1,52 @@
/*
* 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 <Atom/Feature/DiffuseGlobalIllumination/DiffuseGlobalIlluminationFeatureProcessorInterface.h>
namespace AZ
{
namespace Render
{
//! This class provides general features and configuration for the diffuse global illumination environment,
//! which consists of DiffuseProbeGrids and the diffuse Global IBL cubemap.
class DiffuseGlobalIlluminationFeatureProcessor final
: public DiffuseGlobalIlluminationFeatureProcessorInterface
{
public:
AZ_RTTI(AZ::Render::DiffuseGlobalIlluminationFeatureProcessor, "{14F7DF46-AA2C-49EF-8A2C-0A7CB7390BB7}", DiffuseGlobalIlluminationFeatureProcessorInterface);
static void Reflect(AZ::ReflectContext* context);
DiffuseGlobalIlluminationFeatureProcessor() = default;
virtual ~DiffuseGlobalIlluminationFeatureProcessor() = default;
void Activate() override;
void Deactivate() override;
// DiffuseGlobalIlluminationFeatureProcessorInterface overrides
void SetQualityLevel(DiffuseGlobalIlluminationQualityLevel qualityLevel) override;
private:
AZ_DISABLE_COPY_MOVE(DiffuseGlobalIlluminationFeatureProcessor);
// RPI::SceneNotificationBus::Handler overrides
void OnRenderPipelinePassesChanged(RPI::RenderPipeline* renderPipeline) override;
void OnRenderPipelineAdded(RPI::RenderPipelinePtr pipeline) override;
void UpdatePasses();
DiffuseGlobalIlluminationQualityLevel m_qualityLevel = DiffuseGlobalIlluminationQualityLevel::Low;
};
} // namespace Render
} // namespace AZ

@ -12,7 +12,7 @@
#include <Atom/RHI.Reflect/ShaderResourceGroupLayoutDescriptor.h>
#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
#include <DiffuseProbeGrid/DiffuseProbeGrid.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGrid.h>
#include <Atom/RPI.Public/Image/StreamingImage.h>
#include <Atom/RPI.Public/View.h>
#include <Atom/RHI/RHISystemInterface.h>

@ -17,7 +17,7 @@
#include <Atom/RPI.Public/Scene.h>
#include <AzCore/Math/Random.h>
#include <AzCore/Math/Aabb.h>
#include <DiffuseProbeGrid/DiffuseProbeGridTextureReadback.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridTextureReadback.h>
namespace AZ
{

@ -10,7 +10,6 @@
*
*/
#include <DiffuseProbeGrid/DiffuseProbeGridBlendDistancePass.h>
#include <Atom/RHI/Factory.h>
#include <Atom/RHI/FrameGraphInterface.h>
#include <Atom/RHI/FrameGraphAttachmentInterface.h>
@ -18,7 +17,8 @@
#include <Atom/RPI.Public/Pass/PassUtils.h>
#include <Atom/RPI.Public/RenderPipeline.h>
#include <Atom/RPI.Public/RPIUtils.h>
#include <DiffuseProbeGrid/DiffuseProbeGridFeatureProcessor.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridFeatureProcessor.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridBlendDistancePass.h>
#include <RayTracing/RayTracingFeatureProcessor.h>
namespace AZ

@ -10,7 +10,6 @@
*
*/
#include <DiffuseProbeGrid/DiffuseProbeGridBlendIrradiancePass.h>
#include <Atom/RHI/Factory.h>
#include <Atom/RHI/FrameGraphInterface.h>
#include <Atom/RHI/FrameGraphAttachmentInterface.h>
@ -18,7 +17,8 @@
#include <Atom/RPI.Public/Pass/PassUtils.h>
#include <Atom/RPI.Public/RenderPipeline.h>
#include <Atom/RPI.Public/RPIUtils.h>
#include <DiffuseProbeGrid/DiffuseProbeGridFeatureProcessor.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridFeatureProcessor.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridBlendIrradiancePass.h>
#include <RayTracing/RayTracingFeatureProcessor.h>
namespace AZ

@ -10,7 +10,6 @@
*
*/
#include <DiffuseProbeGrid/DiffuseProbeGridBorderUpdatePass.h>
#include <Atom/RHI/Factory.h>
#include <Atom/RHI/FrameGraphInterface.h>
#include <Atom/RHI/FrameGraphAttachmentInterface.h>
@ -18,7 +17,8 @@
#include <Atom/RPI.Public/Pass/PassUtils.h>
#include <Atom/RPI.Public/RenderPipeline.h>
#include <Atom/RPI.Public/RPIUtils.h>
#include <DiffuseProbeGrid/DiffuseProbeGridFeatureProcessor.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridFeatureProcessor.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridBorderUpdatePass.h>
#include <RayTracing/RayTracingFeatureProcessor.h>
namespace AZ

@ -10,7 +10,6 @@
*
*/
#include <DiffuseProbeGrid/DiffuseProbeGridClassificationPass.h>
#include <Atom/RHI/Factory.h>
#include <Atom/RHI/PipelineState.h>
#include <Atom/RHI/FrameGraphInterface.h>
@ -22,7 +21,8 @@
#include <Atom/RPI.Reflect/Pass/PassTemplate.h>
#include <Atom/RPI.Public/View.h>
#include <Atom/RPI.Public/Scene.h>
#include <DiffuseProbeGrid/DiffuseProbeGridFeatureProcessor.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridFeatureProcessor.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGridClassificationPass.h>
#include <RayTracing/RayTracingFeatureProcessor.h>
namespace AZ

@ -12,7 +12,6 @@
#pragma once
#include <AzCore/Memory/SystemAllocator.h>
#include <Atom/RHI/CommandList.h>
#include <Atom/RHI/DrawItem.h>
#include <Atom/RHI/ScopeProducer.h>
@ -22,7 +21,7 @@
#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
#include <Atom/RPI.Reflect/Pass/PassName.h>
#include <Atom/RPI.Public/Image/AttachmentImage.h>
#include <DiffuseProbeGrid/DiffuseProbeGrid.h>
#include <DiffuseGlobalIllumination/DiffuseProbeGrid.h>
namespace AZ
{

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save