Merge branch 'development' into Prism/ShowRepoGems

monroegm-disable-blank-issue-2
nggieber 4 years ago
commit 673815c58d

@ -8,11 +8,14 @@
## Deploy CDK Applications
1. Go to the AWS IAM console and create an IAM role called o3de-automation-tests which adds your own account as as a trusted entity and uses the "AdministratorAccess" permissions policy.
2. Copy {engine_root}\scripts\build\Platform\Windows\deploy_cdk_applications.cmd to your engine root folder.
3. Open a new Command Prompt window at the engine root and set the following environment variables:
3. Open a new Command Prompt window at the engine root and set the following environment variables:
```
Set O3DE_AWS_PROJECT_NAME=AWSAUTO
Set O3DE_AWS_DEPLOY_REGION=us-east-1
Set O3DE_AWS_DEPLOY_ACCOUNT={your_aws_account_id}
Set ASSUME_ROLE_ARN=arn:aws:iam::{your_aws_account_id}:role/o3de-automation-tests
Set COMMIT_ID=HEAD
```
4. In the same Command Prompt window, Deploy the CDK applications for AWS gems by running deploy_cdk_applications.cmd.
## Run Automation Tests

@ -17,3 +17,392 @@ LIGHT_TYPES = {
'simple_point': 6,
'simple_spot': 7,
}
class AtomComponentProperties:
"""
Holds Atom component related constants
"""
@staticmethod
def actor(property: str = 'name') -> str:
"""
Actor component properties.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Actor',
}
return properties[property]
@staticmethod
def bloom(property: str = 'name') -> str:
"""
Bloom component properties. Requires PostFX Layer component.
- 'requires' a list of component names as strings required by this component.
Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Bloom',
'requires': [AtomComponentProperties.postfx_layer()],
}
return properties[property]
@staticmethod
def camera(property: str = 'name') -> str:
"""
Camera component properties.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Camera',
}
return properties[property]
@staticmethod
def decal(property: str = 'name') -> str:
"""
Decal component properties.
- 'Material' the material Asset.id of the decal.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Decal',
'Material': 'Controller|Configuration|Material',
}
return properties[property]
@staticmethod
def deferred_fog(property: str = 'name') -> str:
"""
Deferred Fog component properties. Requires PostFX Layer component.
- 'requires' a list of component names as strings required by this component.
Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Deferred Fog',
'requires': [AtomComponentProperties.postfx_layer()],
}
return properties[property]
@staticmethod
def depth_of_field(property: str = 'name') -> str:
"""
Depth of Field component properties. Requires PostFX Layer component.
- 'requires' a list of component names as strings required by this component.
Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n
- 'Camera Entity' an EditorEntity.id reference to the Camera component required for this effect.
Must be a different entity than the one which hosts Depth of Field component.\n
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'DepthOfField',
'requires': [AtomComponentProperties.postfx_layer()],
'Camera Entity': 'Controller|Configuration|Camera Entity',
}
return properties[property]
@staticmethod
def diffuse_probe(property: str = 'name') -> str:
"""
Diffuse Probe Grid component properties. Requires one of 'shapes'.
- 'shapes' a list of supported shapes as component names.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Diffuse Probe Grid',
'shapes': ['Axis Aligned Box Shape', 'Box Shape']
}
return properties[property]
@staticmethod
def directional_light(property: str = 'name') -> str:
"""
Directional Light component properties.
- 'Camera' an EditorEntity.id reference to the Camera component that controls cascaded shadow view frustum.
Must be a different entity than the one which hosts Directional Light component.\n
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Directional Light',
'Camera': 'Controller|Configuration|Shadow|Camera',
}
return properties[property]
@staticmethod
def display_mapper(property: str = 'name') -> str:
"""
Display Mapper component properties.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Display Mapper',
}
return properties[property]
@staticmethod
def entity_reference(property: str = 'name') -> str:
"""
Entity Reference component properties.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Entity Reference',
}
return properties[property]
@staticmethod
def exposure_control(property: str = 'name') -> str:
"""
Exposure Control component properties. Requires PostFX Layer component.
- 'requires' a list of component names as strings required by this component.
Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Exposure Control',
'requires': [AtomComponentProperties.postfx_layer()],
}
return properties[property]
@staticmethod
def global_skylight(property: str = 'name') -> str:
"""
Global Skylight (IBL) component properties.
- 'Diffuse Image' Asset.id for the cubemap image for determining diffuse lighting.
- 'Specular Image' Asset.id for the cubemap image for determining specular lighting.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Global Skylight (IBL)',
'Diffuse Image': 'Controller|Configuration|Diffuse Image',
'Specular Image': 'Controller|Configuration|Specular Image',
}
return properties[property]
@staticmethod
def grid(property: str = 'name') -> str:
"""
Grid component properties.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Grid',
}
return properties[property]
@staticmethod
def hdr_color_grading(property: str = 'name') -> str:
"""
HDR Color Grading component properties. Requires PostFX Layer component.
- 'requires' a list of component names as strings required by this component.
Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'HDR Color Grading',
'requires': [AtomComponentProperties.postfx_layer()],
}
return properties[property]
@staticmethod
def hdri_skybox(property: str = 'name') -> str:
"""
HDRi Skybox component properties.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'HDRi Skybox',
}
return properties[property]
@staticmethod
def light(property: str = 'name') -> str:
"""
Light component properties.
- 'Light type' from atom_constants.py LIGHT_TYPES
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Light',
'Light type': 'Controller|Configuration|Light type',
}
return properties[property]
@staticmethod
def look_modification(property: str = 'name') -> str:
"""
Look Modification component properties. Requires PostFX Layer component.
- 'requires' a list of component names as strings required by this component.
Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Look Modification',
'requires': [AtomComponentProperties.postfx_layer()],
}
return properties[property]
@staticmethod
def material(property: str = 'name') -> str:
"""
Material component properties. Requires one of Actor OR Mesh component.
- 'requires' a list of component names as strings required by this component.
Only one of these is required at a time for this component.\n
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Material',
'requires': [AtomComponentProperties.actor(), AtomComponentProperties.mesh()],
}
return properties[property]
@staticmethod
def mesh(property: str = 'name') -> str:
"""
Mesh component properties.
- 'Mesh Asset' Asset.id of the mesh model.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
:rtype: str
"""
properties = {
'name': 'Mesh',
'Mesh Asset': 'Controller|Configuration|Mesh Asset',
}
return properties[property]
@staticmethod
def occlusion_culling_plane(property: str = 'name') -> str:
"""
Occlusion Culling Plane component properties.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Occlusion Culling Plane',
}
return properties[property]
@staticmethod
def physical_sky(property: str = 'name') -> str:
"""
Physical Sky component properties.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Physical Sky',
}
return properties[property]
@staticmethod
def postfx_layer(property: str = 'name') -> str:
"""
PostFX Layer component properties.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'PostFX Layer',
}
return properties[property]
@staticmethod
def postfx_gradient(property: str = 'name') -> str:
"""
PostFX Gradient Weight Modifier component properties. Requires PostFX Layer component.
- 'requires' a list of component names as strings required by this component.
Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'PostFX Gradient Weight Modifier',
'requires': [AtomComponentProperties.postfx_layer()],
}
return properties[property]
@staticmethod
def postfx_radius(property: str = 'name') -> str:
"""
PostFX Radius Weight Modifier component properties. Requires PostFX Layer component.
- 'requires' a list of component names as strings required by this component.
Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'PostFX Radius Weight Modifier',
'requires': [AtomComponentProperties.postfx_layer()],
}
return properties[property]
@staticmethod
def postfx_shape(property: str = 'name') -> str:
"""
PostFX Shape Weight Modifier component properties. Requires PostFX Layer and one of 'shapes' listed.
- 'requires' a list of component names as strings required by this component.
Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n
- 'shapes' a list of supported shapes as component names. 'Tube Shape' is also supported but requires 'Spline'.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'PostFX Shape Weight Modifier',
'requires': [AtomComponentProperties.postfx_layer()],
'shapes': ['Axis Aligned Box Shape', 'Box Shape', 'Capsule Shape', 'Compound Shape', 'Cylinder Shape',
'Disk Shape', 'Polygon Prism Shape', 'Quad Shape', 'Sphere Shape', 'Vegetation Reference Shape'],
}
return properties[property]
@staticmethod
def reflection_probe(property: str = 'name') -> str:
"""
Reflection Probe component properties. Requires one of 'shapes' listed.
- 'shapes' a list of supported shapes as component names.
- 'Baked Cubemap Path' Asset.id of the baked cubemap image generated by a call to 'BakeReflectionProbe' ebus.
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'Reflection Probe',
'shapes': ['Axis Aligned Box Shape', 'Box Shape'],
'Baked Cubemap Path': 'Cubemap|Baked Cubemap Path',
}
return properties[property]
@staticmethod
def ssao(property: str = 'name') -> str:
"""
SSAO component properties. Requires PostFX Layer component.
- 'requires' a list of component names as strings required by this component.
Use editor_entity_utils EditorEntity.add_components(list) to add this list of requirements.\n
:param property: From the last element of the property tree path. Default 'name' for component name string.
:return: Full property path OR component name if no property specified.
"""
properties = {
'name': 'SSAO',
'requires': [AtomComponentProperties.postfx_layer()],
}
return properties[property]

@ -80,25 +80,25 @@ def AtomEditorComponents_Mesh_AddedToEntity():
from editor_python_test_tools.asset_utils import Asset
from editor_python_test_tools.editor_entity_utils import EditorEntity
from editor_python_test_tools.utils import Report, Tracer, TestHelper as helper
from editor_python_test_tools.utils import Report, Tracer, TestHelper
from Atom.atom_utils.atom_constants import AtomComponentProperties as Atom
with Tracer() as error_tracer:
# Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
helper.init_idle()
helper.open_level("", "Base")
TestHelper.init_idle()
TestHelper.open_level("", "Base")
# Test steps begin.
# 1. Create a Mesh entity with no components.
mesh_name = "Mesh"
mesh_entity = EditorEntity.create_editor_entity(mesh_name)
mesh_entity = EditorEntity.create_editor_entity(Atom.mesh())
Report.critical_result(Tests.mesh_entity_creation, mesh_entity.exists())
# 2. Add a Mesh component to Mesh entity.
mesh_component = mesh_entity.add_component(mesh_name)
mesh_component = mesh_entity.add_component(Atom.mesh())
Report.critical_result(
Tests.mesh_component_added,
mesh_entity.has_component(mesh_name))
mesh_entity.has_component(Atom.mesh()))
# 3. UNDO the entity creation and component addition.
# -> UNDO component addition.
@ -125,17 +125,16 @@ def AtomEditorComponents_Mesh_AddedToEntity():
Report.result(Tests.creation_redo, mesh_entity.exists())
# 5. Set Mesh component asset property
mesh_property_asset = 'Controller|Configuration|Mesh Asset'
model_path = os.path.join('Objects', 'shaderball', 'shaderball_default_1m.azmodel')
model = Asset.find_asset_by_path(model_path)
mesh_component.set_component_property_value(mesh_property_asset, model.id)
mesh_component.set_component_property_value(Atom.mesh('Mesh Asset'), model.id)
Report.result(Tests.mesh_asset_specified,
mesh_component.get_component_property_value(mesh_property_asset) == model.id)
mesh_component.get_component_property_value(Atom.mesh('Mesh Asset')) == model.id)
# 6. Enter/Exit game mode.
helper.enter_game_mode(Tests.enter_game_mode)
TestHelper.enter_game_mode(Tests.enter_game_mode)
general.idle_wait_frames(1)
helper.exit_game_mode(Tests.exit_game_mode)
TestHelper.exit_game_mode(Tests.exit_game_mode)
# 7. Test IsHidden.
mesh_entity.set_visibility_state(False)
@ -159,7 +158,7 @@ def AtomEditorComponents_Mesh_AddedToEntity():
Report.result(Tests.deletion_redo, not mesh_entity.exists())
# 12. Look for errors or asserts.
helper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0)
for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts:

@ -16,18 +16,11 @@
#include <QScopedValueRollback>
#include <QToolBar>
#include <QLoggingCategory>
#if defined(AZ_PLATFORM_WINDOWS)
#include <QtGui/qpa/qplatformnativeinterface.h>
#include <QtGui/private/qhighdpiscaling_p.h>
#endif
#include <AzCore/Component/ComponentApplication.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
// AzFramework
#if defined(AZ_PLATFORM_WINDOWS)
# include <AzFramework/Input/Buses/Notifications/RawInputNotificationBus_Platform.h>
#endif // defined(AZ_PLATFORM_WINDOWS)
// AzQtComponents
#include <AzQtComponents/Components/GlobalEventFilter.h>
@ -39,7 +32,6 @@
#include "Settings.h"
#include "CryEdit.h"
enum
{
// in milliseconds
@ -241,7 +233,6 @@ namespace Editor
EditorQtApplication::EditorQtApplication(int& argc, char** argv)
: AzQtApplication(argc, argv)
, m_inWinEventFilter(false)
, m_stylesheet(new AzQtComponents::O3DEStylesheet(this))
, m_idleTimer(new QTimer(this))
{
@ -368,86 +359,10 @@ namespace Editor
UninstallEditorTranslators();
}
#if defined(AZ_PLATFORM_WINDOWS)
bool EditorQtApplication::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long* result)
EditorQtApplication* EditorQtApplication::instance()
{
MSG* msg = (MSG*)message;
if (msg->message == WM_MOVING || msg->message == WM_SIZING)
{
m_isMovingOrResizing = true;
}
else if (msg->message == WM_EXITSIZEMOVE)
{
m_isMovingOrResizing = false;
}
// Prevent the user from being able to move the window in game mode.
// This is done during the hit test phase to bypass the native window move messages. If the window
// decoration wrapper title bar contains the cursor, set the result to HTCLIENT instead of
// HTCAPTION.
if (msg->message == WM_NCHITTEST && GetIEditor()->IsInGameMode())
{
const LRESULT defWinProcResult = DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam);
if (defWinProcResult == 1)
{
if (QWidget* widget = QWidget::find((WId)msg->hwnd))
{
if (auto wrapper = qobject_cast<const AzQtComponents::WindowDecorationWrapper *>(widget))
{
AzQtComponents::TitleBar* titleBar = wrapper->titleBar();
const short global_x = static_cast<short>(LOWORD(msg->lParam));
const short global_y = static_cast<short>(HIWORD(msg->lParam));
const QPoint globalPos = QHighDpi::fromNativePixels(QPoint(global_x, global_y), widget->window()->windowHandle());
const QPoint local = titleBar->mapFromGlobal(globalPos);
if (titleBar->draggableRect().contains(local) && !titleBar->isTopResizeArea(globalPos))
{
*result = HTCLIENT;
return true;
}
}
}
}
}
// Ensure that the Windows WM_INPUT messages get passed through to the AzFramework input system.
// These events are only broadcast in game mode. In Editor mode, RenderViewportWidget creates synthetic
// keyboard and mouse events via Qt.
if (GetIEditor()->IsInGameMode())
{
if (msg->message == WM_INPUT)
{
UINT rawInputSize;
const UINT rawInputHeaderSize = sizeof(RAWINPUTHEADER);
GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, nullptr, &rawInputSize, rawInputHeaderSize);
AZStd::array<BYTE, sizeof(RAWINPUT)> rawInputBytesArray;
LPBYTE rawInputBytes = rawInputBytesArray.data();
[[maybe_unused]] const UINT bytesCopied = GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, rawInputBytes, &rawInputSize, rawInputHeaderSize);
CRY_ASSERT(bytesCopied == rawInputSize);
RAWINPUT* rawInput = (RAWINPUT*)rawInputBytes;
CRY_ASSERT(rawInput);
AzFramework::RawInputNotificationBusWindows::Broadcast(&AzFramework::RawInputNotificationsWindows::OnRawInputEvent, *rawInput);
return false;
}
else if (msg->message == WM_DEVICECHANGE)
{
if (msg->wParam == 0x0007) // DBT_DEVNODES_CHANGED
{
AzFramework::RawInputNotificationBusWindows::Broadcast(&AzFramework::RawInputNotificationsWindows::OnRawInputDeviceChangeEvent);
}
return true;
}
}
return false;
return static_cast<EditorQtApplication*>(QApplication::instance());
}
#endif
void EditorQtApplication::OnEditorNotifyEvent(EEditorNotifyEvent event)
{
@ -505,11 +420,6 @@ namespace Editor
return m_stylesheet->GetColorByName(name);
}
EditorQtApplication* EditorQtApplication::instance()
{
return static_cast<EditorQtApplication*>(QApplication::instance());
}
bool EditorQtApplication::IsActive()
{
return applicationState() == Qt::ApplicationActive;
@ -613,42 +523,6 @@ namespace Editor
case QEvent::KeyRelease:
m_pressedKeys.remove(reinterpret_cast<QKeyEvent*>(event)->key());
break;
#ifdef AZ_PLATFORM_WINDOWS
case QEvent::Leave:
{
// if we receive a leave event for a toolbar on Windows
// check first whether we really left it. If we didn't: start checking
// for the tool bar under the mouse by timer to check when we really left.
// Synthesize a new leave event then. Workaround for LY-69788
auto toolBarAt = [](const QPoint& pos) -> QToolBar* {
QWidget* widget = qApp->widgetAt(pos);
while (widget != nullptr)
{
if (QToolBar* tb = qobject_cast<QToolBar*>(widget))
{
return tb;
}
widget = widget->parentWidget();
}
return nullptr;
};
if (object == toolBarAt(QCursor::pos()))
{
QTimer* t = new QTimer(object);
t->start(100);
connect(t, &QTimer::timeout, object, [t, object, toolBarAt]() {
if (object != toolBarAt(QCursor::pos()))
{
QEvent event(QEvent::Leave);
qApp->sendEvent(object, &event);
t->deleteLater();
}
});
return true;
}
break;
}
#endif
default:
break;
}

@ -72,14 +72,12 @@ namespace Editor
////
static EditorQtApplication* instance();
static EditorQtApplication* newInstance(int& argc, char** argv);
static bool IsActive();
bool isMovingOrResizing() const;
// QAbstractNativeEventFilter:
bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override;
// IEditorNotifyListener:
void OnEditorNotifyEvent(EEditorNotifyEvent event) override;
@ -100,6 +98,10 @@ namespace Editor
signals:
void skinChanged();
protected:
bool m_isMovingOrResizing = false;
private:
enum TimerResetFlag
{
@ -116,8 +118,6 @@ namespace Editor
AzQtComponents::O3DEStylesheet* m_stylesheet;
bool m_inWinEventFilter = false;
// Translators
void InstallEditorTranslators();
void UninstallEditorTranslators();
@ -127,7 +127,6 @@ namespace Editor
QTranslator* m_editorTranslator = nullptr;
QTranslator* m_assetBrowserTranslator = nullptr;
QTimer* const m_idleTimer = nullptr;
bool m_isMovingOrResizing = false;
AZ::UserSettingsProvider m_localUserSettings;

@ -4135,9 +4135,9 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[])
Editor::EditorQtApplication::InstallQtLogHandler();
AzQtComponents::Utilities::HandleDpiAwareness(AzQtComponents::Utilities::SystemDpiAware);
Editor::EditorQtApplication app(argc, argv);
Editor::EditorQtApplication* app = Editor::EditorQtApplication::newInstance(argc, argv);
if (app.arguments().contains("-autotest_mode"))
if (app->arguments().contains("-autotest_mode"))
{
// Nullroute all stdout to null for automated tests, this way we make sure
// that the test result output is not polluted with unrelated output data.
@ -4173,12 +4173,7 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[])
return -1;
}
AzToolsFramework::EditorEvents::Bus::Broadcast(&AzToolsFramework::EditorEvents::NotifyQtApplicationAvailable, &app);
#if defined(AZ_PLATFORM_MAC)
// Native menu bars do not work on macOS due to all the tool dialogs
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
#endif
AzToolsFramework::EditorEvents::Bus::Broadcast(&AzToolsFramework::EditorEvents::NotifyQtApplicationAvailable, app);
int exitCode = 0;
@ -4189,9 +4184,9 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[])
if (didCryEditStart)
{
app.EnableOnIdle();
app->EnableOnIdle();
ret = app.exec();
ret = app->exec();
}
else
{
@ -4202,6 +4197,8 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[])
}
delete app;
gSettings.Disconnect();
return ret;

@ -6,7 +6,7 @@
*
*/
#include "QtEditorApplication.h"
#include "QtEditorApplication_linux.h"
#ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
#include <AzFramework/XcbEventHandler.h>
@ -14,7 +14,16 @@
namespace Editor
{
bool EditorQtApplication::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long*)
EditorQtApplication* EditorQtApplication::newInstance(int& argc, char** argv)
{
#ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
return new EditorQtApplicationXcb(argc, argv);
#endif
return nullptr;
}
bool EditorQtApplicationXcb::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long*)
{
if (GetIEditor()->IsInGameMode())
{

@ -0,0 +1,25 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <Editor/Core/QtEditorApplication.h>
namespace Editor
{
class EditorQtApplicationXcb : public EditorQtApplication
{
Q_OBJECT
public:
EditorQtApplicationXcb(int& argc, char** argv)
: EditorQtApplication(argc, argv)
{
}
// QAbstractNativeEventFilter:
bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override;
};
} // namespace Editor

@ -7,6 +7,6 @@
#
set(FILES
../../Core/QtEditorApplication_linux.cpp
Editor/Core/QtEditorApplication_linux.cpp
../Common/Unimplemented/Util/Mailer_Unimplemented.cpp
)

@ -0,0 +1,25 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <Editor/Core/QtEditorApplication.h>
namespace Editor
{
class EditorQtApplicationMac : public EditorQtApplication
{
Q_OBJECT
public:
EditorQtApplicationMac(int& argc, char** argv)
: EditorQtApplication(argc, argv)
{
}
// QAbstractNativeEventFilter:
bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override;
};
} // namespace Editor

@ -19,7 +19,14 @@
namespace Editor
{
bool EditorQtApplication::nativeEventFilter(const QByteArray& eventType, void* message, long* result)
EditorQtApplication* EditorQtApplication::newInstance(int& argc, char** argv)
{
QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar);
return new EditorQtApplicationMac(argc, argv);
}
bool EditorQtApplicationMac::nativeEventFilter(const QByteArray& eventType, void* message, long* result)
{
NSEvent* event = (NSEvent*)message;
if (GetIEditor()->IsInGameMode())

@ -7,7 +7,7 @@
#
set(FILES
../../Core/QtEditorApplication_mac.mm
Editor/Core/QtEditorApplication_mac.mm
../../LogFile_mac.mm
../../WindowObserver_mac.h
../../WindowObserver_mac.mm

@ -0,0 +1,165 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "QtEditorApplication_windows.h"
// Qt
#include <QAbstractEventDispatcher>
#include <QScopedValueRollback>
#include <QToolBar>
#include <QLoggingCategory>
#include <QTimer>
#include <QtGui/private/qhighdpiscaling_p.h>
#include <QtGui/qpa/qplatformnativeinterface.h>
// AzQtComponents
#include <AzQtComponents/Components/Titlebar.h>
#include <AzQtComponents/Components/WindowDecorationWrapper.h>
// AzFramework
#include <AzFramework/Input/Buses/Notifications/RawInputNotificationBus_Platform.h>
namespace Editor
{
EditorQtApplication* EditorQtApplication::newInstance(int& argc, char** argv)
{
return new EditorQtApplicationWindows(argc, argv);
}
bool EditorQtApplicationWindows::nativeEventFilter([[maybe_unused]] const QByteArray& eventType, void* message, long* result)
{
MSG* msg = (MSG*)message;
if (msg->message == WM_MOVING || msg->message == WM_SIZING)
{
m_isMovingOrResizing = true;
}
else if (msg->message == WM_EXITSIZEMOVE)
{
m_isMovingOrResizing = false;
}
// Prevent the user from being able to move the window in game mode.
// This is done during the hit test phase to bypass the native window move messages. If the window
// decoration wrapper title bar contains the cursor, set the result to HTCLIENT instead of
// HTCAPTION.
if (msg->message == WM_NCHITTEST && GetIEditor()->IsInGameMode())
{
const LRESULT defWinProcResult = DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam);
if (defWinProcResult == 1)
{
if (QWidget* widget = QWidget::find((WId)msg->hwnd))
{
if (auto wrapper = qobject_cast<const AzQtComponents::WindowDecorationWrapper*>(widget))
{
AzQtComponents::TitleBar* titleBar = wrapper->titleBar();
const short global_x = static_cast<short>(LOWORD(msg->lParam));
const short global_y = static_cast<short>(HIWORD(msg->lParam));
const QPoint globalPos = QHighDpi::fromNativePixels(QPoint(global_x, global_y), widget->window()->windowHandle());
const QPoint local = titleBar->mapFromGlobal(globalPos);
if (titleBar->draggableRect().contains(local) && !titleBar->isTopResizeArea(globalPos))
{
*result = HTCLIENT;
return true;
}
}
}
}
}
// Ensure that the Windows WM_INPUT messages get passed through to the AzFramework input system.
// These events are only broadcast in game mode. In Editor mode, RenderViewportWidget creates synthetic
// keyboard and mouse events via Qt.
if (GetIEditor()->IsInGameMode())
{
if (msg->message == WM_INPUT)
{
UINT rawInputSize;
const UINT rawInputHeaderSize = sizeof(RAWINPUTHEADER);
GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, nullptr, &rawInputSize, rawInputHeaderSize);
AZStd::array<BYTE, sizeof(RAWINPUT)> rawInputBytesArray;
LPBYTE rawInputBytes = rawInputBytesArray.data();
[[maybe_unused]] const UINT bytesCopied =
GetRawInputData((HRAWINPUT)msg->lParam, RID_INPUT, rawInputBytes, &rawInputSize, rawInputHeaderSize);
CRY_ASSERT(bytesCopied == rawInputSize);
RAWINPUT* rawInput = (RAWINPUT*)rawInputBytes;
CRY_ASSERT(rawInput);
AzFramework::RawInputNotificationBusWindows::Broadcast(
&AzFramework::RawInputNotificationsWindows::OnRawInputEvent, *rawInput);
return false;
}
else if (msg->message == WM_DEVICECHANGE)
{
if (msg->wParam == 0x0007) // DBT_DEVNODES_CHANGED
{
AzFramework::RawInputNotificationBusWindows::Broadcast(
&AzFramework::RawInputNotificationsWindows::OnRawInputDeviceChangeEvent);
}
return true;
}
}
return false;
}
bool EditorQtApplicationWindows::eventFilter(QObject* object, QEvent* event)
{
switch (event->type())
{
case QEvent::Leave:
{
// if we receive a leave event for a toolbar on Windows
// check first whether we really left it. If we didn't: start checking
// for the tool bar under the mouse by timer to check when we really left.
// Synthesize a new leave event then. Workaround for LY-69788
auto toolBarAt = [](const QPoint& pos) -> QToolBar*
{
QWidget* widget = qApp->widgetAt(pos);
while (widget != nullptr)
{
if (QToolBar* tb = qobject_cast<QToolBar*>(widget))
{
return tb;
}
widget = widget->parentWidget();
}
return false;
};
if (object == toolBarAt(QCursor::pos()))
{
QTimer* t = new QTimer(object);
t->start(100);
connect(
t, &QTimer::timeout, object,
[t, object, toolBarAt]()
{
if (object != toolBarAt(QCursor::pos()))
{
QEvent event(QEvent::Leave);
qApp->sendEvent(object, &event);
t->deleteLater();
}
});
return true;
}
break;
}
default:
break;
}
return EditorQtApplication::eventFilter(object, event);
}
} // namespace Editor

@ -0,0 +1,27 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <Editor/Core/QtEditorApplication.h>
namespace Editor
{
class EditorQtApplicationWindows : public EditorQtApplication
{
Q_OBJECT
public:
EditorQtApplicationWindows(int& argc, char** argv)
: EditorQtApplication(argc, argv)
{
}
// QAbstractNativeEventFilter:
bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override;
bool eventFilter(QObject* object, QEvent* event) override;
};
} // namespace Editor

@ -7,5 +7,6 @@
#
set(FILES
Editor/Core/QtEditorApplication_windows.cpp
Util/Mailer_Windows.cpp
)

@ -0,0 +1,97 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/Math/Vector3.h>
#include <AzCore/EBus/EBus.h>
#include <AzCore/Component/ComponentBus.h>
#include <AzCore/Math/Aabb.h>
#include <AzFramework/Physics/Material.h>
namespace Physics
{
//! The QuadMeshType specifies the property of the heightfield quad.
enum class QuadMeshType : uint8_t
{
SubdivideUpperLeftToBottomRight, //!< Subdivide the quad, from upper left to bottom right |\|, into two triangles.
SubdivideBottomLeftToUpperRight, //!< Subdivide the quad, from bottom left to upper right |/|, into two triangles.
Hole //!< The quad should be treated as a hole in the heightfield.
};
struct HeightMaterialPoint
{
float m_height{ 0.0f }; //!< Holds the height of this point in the heightfield relative to the heightfield entity location.
QuadMeshType m_quadMeshType{ QuadMeshType::SubdivideUpperLeftToBottomRight }; //!< By default, create two triangles like this |\|, where this point is in the upper left corner.
uint8_t m_materialIndex{ 0 }; //!< The surface material index for the upper left corner of this quad.
uint16_t m_padding{ 0 }; //!< available for future use.
};
//! An interface to provide heightfield values.
class HeightfieldProviderRequests
: public AZ::ComponentBus
{
public:
//! Returns the distance between each height in the map.
//! @return Vector containing Column Spacing, Rows Spacing.
virtual AZ::Vector2 GetHeightfieldGridSpacing() const = 0;
//! Returns the height field gridsize.
//! @param numColumns contains the size of the grid in the x direction.
//! @param numRows contains the size of the grid in the y direction.
virtual void GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const = 0;
//! Returns the height field min and max height bounds.
//! @param minHeightBounds contains the minimum height that the heightfield can contain.
//! @param maxHeightBounds contains the maximum height that the heightfield can contain.
virtual void GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const = 0;
//! Returns the AABB of the heightfield.
//! This is provided separately from the shape AABB because the heightfield might choose to modify the AABB bounds.
//! @return AABB of the heightfield.
virtual AZ::Aabb GetHeightfieldAabb() const = 0;
//! Returns the world transform for the heightfield.
//! This is provided separately from the entity transform because the heightfield might want to clear out the rotation or scale.
//! @return world transform that should be used with the heightfield data.
virtual AZ::Transform GetHeightfieldTransform() const = 0;
//! Returns the list of materials used by the height field.
//! @return returns a vector of all materials.
virtual AZStd::vector<MaterialId> GetMaterialList() const = 0;
//! Returns the list of heights used by the height field.
//! @return the rows*columns vector of the heights.
virtual AZStd::vector<float> GetHeights() const = 0;
//! Returns the list of heights and materials used by the height field.
//! @return the rows*columns vector of the heights and materials.
virtual AZStd::vector<Physics::HeightMaterialPoint> GetHeightsAndMaterials() const = 0;
};
using HeightfieldProviderRequestsBus = AZ::EBus<HeightfieldProviderRequests>;
//! Broadcasts notifications when heightfield data changes - heightfield providers implement HeightfieldRequests bus.
class HeightfieldProviderNotifications
: public AZ::ComponentBus
{
public:
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple;
//! Called whenever the heightfield data changes.
//! @param the AABB of the area of data that changed.
virtual void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion)
{
}
protected:
~HeightfieldProviderNotifications() = default;
};
using HeightfieldProviderNotificationBus = AZ::EBus<HeightfieldProviderNotifications>;
} // namespace Physics

@ -0,0 +1,33 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <gmock/gmock.h>
#include <AzFramework/Physics/HeightfieldProviderBus.h>
namespace UnitTest
{
class MockHeightfieldProviderNotificationBusListener
: private Physics::HeightfieldProviderNotificationBus::Handler
{
public:
MockHeightfieldProviderNotificationBusListener(AZ::EntityId entityid)
{
Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityid);
}
~MockHeightfieldProviderNotificationBusListener()
{
Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect();
}
MOCK_METHOD1(OnHeightfieldDataChanged, void(const AZ::Aabb&));
};
} // namespace UnitTest

@ -37,6 +37,7 @@ namespace Physics
REFLECT_SHAPETYPE_ENUM_VALUE(Sphere);
REFLECT_SHAPETYPE_ENUM_VALUE(Cylinder);
REFLECT_SHAPETYPE_ENUM_VALUE(PhysicsAsset);
REFLECT_SHAPETYPE_ENUM_VALUE(Heightfield);
#undef REFLECT_SHAPETYPE_ENUM_VALUE
}
@ -305,4 +306,125 @@ namespace Physics
m_cachedNativeMesh = nullptr;
}
}
void HeightfieldShapeConfiguration::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext
->RegisterGenericType<AZStd::shared_ptr<HeightfieldShapeConfiguration>>();
serializeContext->Class<HeightfieldShapeConfiguration, ShapeConfiguration>()
->Version(1);
}
}
HeightfieldShapeConfiguration::~HeightfieldShapeConfiguration()
{
SetCachedNativeHeightfield(nullptr);
}
HeightfieldShapeConfiguration::HeightfieldShapeConfiguration(const HeightfieldShapeConfiguration& other)
: ShapeConfiguration(other)
, m_gridResolution(other.m_gridResolution)
, m_numColumns(other.m_numColumns)
, m_numRows(other.m_numRows)
, m_samples(other.m_samples)
, m_minHeightBounds(other.m_minHeightBounds)
, m_maxHeightBounds(other.m_maxHeightBounds)
, m_cachedNativeHeightfield(nullptr)
{
}
HeightfieldShapeConfiguration& HeightfieldShapeConfiguration::operator=(const HeightfieldShapeConfiguration& other)
{
ShapeConfiguration::operator=(other);
m_gridResolution = other.m_gridResolution;
m_numColumns = other.m_numColumns;
m_numRows = other.m_numRows;
m_samples = other.m_samples;
m_minHeightBounds = other.m_minHeightBounds;
m_maxHeightBounds = other.m_maxHeightBounds;
// Prevent raw pointer from being copied
m_cachedNativeHeightfield = nullptr;
return *this;
}
void* HeightfieldShapeConfiguration::GetCachedNativeHeightfield() const
{
return m_cachedNativeHeightfield;
}
void HeightfieldShapeConfiguration::SetCachedNativeHeightfield(void* cachedNativeHeightfield) const
{
if (m_cachedNativeHeightfield)
{
Physics::SystemRequestBus::Broadcast(&Physics::SystemRequests::ReleaseNativeHeightfieldObject, m_cachedNativeHeightfield);
}
m_cachedNativeHeightfield = cachedNativeHeightfield;
}
AZ::Vector2 HeightfieldShapeConfiguration::GetGridResolution() const
{
return m_gridResolution;
}
void HeightfieldShapeConfiguration::SetGridResolution(const AZ::Vector2& gridResolution)
{
m_gridResolution = gridResolution;
}
int32_t HeightfieldShapeConfiguration::GetNumColumns() const
{
return m_numColumns;
}
void HeightfieldShapeConfiguration::SetNumColumns(int32_t numColumns)
{
m_numColumns = numColumns;
}
int32_t HeightfieldShapeConfiguration::GetNumRows() const
{
return m_numRows;
}
void HeightfieldShapeConfiguration::SetNumRows(int32_t numRows)
{
m_numRows = numRows;
}
const AZStd::vector<Physics::HeightMaterialPoint>& HeightfieldShapeConfiguration::GetSamples() const
{
return m_samples;
}
void HeightfieldShapeConfiguration::SetSamples(const AZStd::vector<Physics::HeightMaterialPoint>& samples)
{
m_samples = samples;
}
float HeightfieldShapeConfiguration::GetMinHeightBounds() const
{
return m_minHeightBounds;
}
void HeightfieldShapeConfiguration::SetMinHeightBounds(float minBounds)
{
m_minHeightBounds = minBounds;
}
float HeightfieldShapeConfiguration::GetMaxHeightBounds() const
{
return m_maxHeightBounds;
}
void HeightfieldShapeConfiguration::SetMaxHeightBounds(float maxBounds)
{
m_maxHeightBounds = maxBounds;
}
}

@ -9,10 +9,13 @@
#pragma once
#include <AzCore/Math/Vector3.h>
#include <AzCore/Math/Vector2.h>
#include <AzCore/Math/Quaternion.h>
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Physics/HeightfieldProviderBus.h>
namespace Physics
{
/// Used to identify shape configuration type from base class.
@ -27,6 +30,7 @@ namespace Physics
Native, ///< Native shape configuration if user wishes to bypass generic shape configurations.
PhysicsAsset, ///< Shapes configured in the asset.
CookedMesh, ///< Stores a blob of mesh data cooked for the specific engine.
Heightfield ///< Interacts with the physics system heightfield
};
class ShapeConfiguration
@ -196,4 +200,52 @@ namespace Physics
mutable void* m_cachedNativeMesh = nullptr;
};
class HeightfieldShapeConfiguration
: public ShapeConfiguration
{
public:
AZ_CLASS_ALLOCATOR(HeightfieldShapeConfiguration, AZ::SystemAllocator, 0);
AZ_RTTI(HeightfieldShapeConfiguration, "{8DF47C83-D2A9-4E7C-8620-5E173E43C0B3}", ShapeConfiguration);
static void Reflect(AZ::ReflectContext* context);
HeightfieldShapeConfiguration() = default;
HeightfieldShapeConfiguration(const HeightfieldShapeConfiguration&);
HeightfieldShapeConfiguration& operator=(const HeightfieldShapeConfiguration&);
~HeightfieldShapeConfiguration();
ShapeType GetShapeType() const override
{
return ShapeType::Heightfield;
}
void* GetCachedNativeHeightfield() const;
void SetCachedNativeHeightfield(void* cachedNativeHeightfield) const;
AZ::Vector2 GetGridResolution() const;
void SetGridResolution(const AZ::Vector2& gridSpacing);
int32_t GetNumColumns() const;
void SetNumColumns(int32_t numColumns);
int32_t GetNumRows() const;
void SetNumRows(int32_t numRows);
const AZStd::vector<Physics::HeightMaterialPoint>& GetSamples() const;
void SetSamples(const AZStd::vector<Physics::HeightMaterialPoint>& samples);
float GetMinHeightBounds() const;
void SetMinHeightBounds(float minBounds);
float GetMaxHeightBounds() const;
void SetMaxHeightBounds(float maxBounds);
private:
//! The number of meters between each heightfield sample.
AZ::Vector2 m_gridResolution{ 1.0f };
//! The number of columns in the heightfield sample grid.
int32_t m_numColumns{ 0 };
//! The number of rows in the heightfield sample grid.
int32_t m_numRows{ 0 };
//! The minimum and maximum heights that can be used by this heightfield.
//! This can be used by the physics system to choose a more optimal heightfield data type internally (ex: int16, uint8)
float m_minHeightBounds{AZStd::numeric_limits<float>::lowest()};
float m_maxHeightBounds{AZStd::numeric_limits<float>::max()};
//! The grid of sample points for the heightfield.
AZStd::vector<Physics::HeightMaterialPoint> m_samples;
//! An optional storage pointer for the physics system to cache its native heightfield representation.
mutable void* m_cachedNativeHeightfield{ nullptr };
};
} // namespace Physics

@ -132,6 +132,10 @@ namespace Physics
virtual AZStd::shared_ptr<Material> CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) = 0;
/// Releases the height field object created by the physics backend.
/// @param nativeHeightfieldObject Pointer to the height field object.
virtual void ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) = 0;
/// Releases the mesh object created by the physics backend.
/// @param nativeMeshObject Pointer to the mesh object.
virtual void ReleaseNativeMeshObject(void* nativeMeshObject) = 0;

@ -107,6 +107,7 @@ namespace Physics
PhysicsAssetShapeConfiguration::Reflect(context);
NativeShapeConfiguration::Reflect(context);
CookedMeshShapeConfiguration::Reflect(context);
HeightfieldShapeConfiguration::Reflect(context);
AzPhysics::SystemInterface::Reflect(context);
AzPhysics::Scene::Reflect(context);
AzPhysics::CollisionLayer::Reflect(context);

@ -0,0 +1,11 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
set(FILES
Mocks/MockHeightfieldProviderBus.h
)

@ -228,6 +228,7 @@ set(FILES
Physics/Configuration/SimulatedBodyConfiguration.cpp
Physics/Configuration/SystemConfiguration.h
Physics/Configuration/SystemConfiguration.cpp
Physics/HeightfieldProviderBus.h
Physics/SimulatedBodies/RigidBody.h
Physics/SimulatedBodies/RigidBody.cpp
Physics/SimulatedBodies/StaticRigidBody.h
@ -251,6 +252,7 @@ set(FILES
Physics/Shape.h
Physics/ShapeConfiguration.h
Physics/ShapeConfiguration.cpp
Physics/HeightfieldProviderBus.h
Physics/SystemBus.h
Physics/ColliderComponentBus.h
Physics/RagdollPhysicsBus.h

@ -42,6 +42,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
NAMESPACE AZ
FILES_CMAKE
Tests/framework_shared_tests_files.cmake
AzFramework/Physics/physics_mock_files.cmake
INCLUDE_DIRECTORIES
PUBLIC
Tests
@ -53,7 +54,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
AZ::AzTest
AZ::AzTestShared
)
if(PAL_TRAIT_BUILD_HOST_TOOLS)
ly_add_target(

@ -13,6 +13,7 @@
#include <QIcon>
#include <QToolButton>
#include <QPropertyAnimation>
#include <QPainter>
namespace AzQtComponents
{
@ -27,6 +28,13 @@ namespace AzQtComponents
setAttribute(Qt::WA_ShowWithoutActivating);
setAttribute(Qt::WA_DeleteOnClose);
m_borderRadius = toastConfiguration.m_borderRadius;
if (m_borderRadius > 0)
{
setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
setAttribute(Qt::WA_TranslucentBackground);
}
m_ui->setupUi(this);
QIcon toastIcon;
@ -53,6 +61,13 @@ namespace AzQtComponents
m_ui->titleLabel->setText(toastConfiguration.m_title);
m_ui->mainLabel->setText(toastConfiguration.m_description);
// hide the optional description if none is provided so the title is centered vertically
if (toastConfiguration.m_description.isEmpty())
{
m_ui->mainLabel->setVisible(false);
m_ui->verticalLayout->removeWidget(m_ui->mainLabel);
}
m_lifeSpan.setInterval(aznumeric_cast<int>(toastConfiguration.m_duration.count()));
m_closeOnClick = toastConfiguration.m_closeOnClick;
@ -68,6 +83,24 @@ namespace AzQtComponents
{
}
void ToastNotification::paintEvent(QPaintEvent* event)
{
if (m_borderRadius > 0)
{
QPainter p(this);
p.setPen(Qt::transparent);
QColor painterColor;
painterColor.setRgbF(0, 0, 0, 255);
p.setBrush(painterColor);
p.setRenderHint(QPainter::Antialiasing);
p.drawRoundedRect(rect(), m_borderRadius, m_borderRadius);
}
else
{
QDialog::paintEvent(event);
}
}
void ToastNotification::ShowToastAtCursor()
{
QPoint globalCursorPos = QCursor::pos();

@ -52,6 +52,8 @@ namespace AzQtComponents
void mousePressEvent(QMouseEvent* mouseEvent) override;
bool eventFilter(QObject* object, QEvent* event) override;
void paintEvent(QPaintEvent* event) override;
public slots:
void StartTimer();
void FadeOut();
@ -65,6 +67,7 @@ namespace AzQtComponents
bool m_closeOnClick;
QTimer m_lifeSpan;
uint32_t m_borderRadius = 0;
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
AZStd::chrono::milliseconds m_fadeDuration;

@ -191,7 +191,7 @@
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<width>20</width>
<height>20</height>
</size>
</property>

@ -37,6 +37,7 @@ namespace AzQtComponents
QString m_title;
QString m_description;
QString m_customIconImage;
uint32_t m_borderRadius = 0;
AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING
AZStd::chrono::milliseconds m_duration = AZStd::chrono::milliseconds(5000);

@ -80,14 +80,7 @@ namespace AzToolsFramework
// set up signals before we start thread.
m_shutdownThreadSignal = false;
// Check to see if the 'p4' command is available at the command line
int p4VersionExitCode = QProcess::execute("p4", QStringList{ "-V" });
m_p4ApplicationDetected = (p4VersionExitCode == 0);
if (m_p4ApplicationDetected)
{
m_WorkerThread = AZStd::thread(AZStd::bind(&PerforceComponent::ThreadWorker, this));
}
m_WorkerThread = AZStd::thread(AZStd::bind(&PerforceComponent::ThreadWorker, this));
SourceControlConnectionRequestBus::Handler::BusConnect();
SourceControlCommandBus::Handler::BusConnect();
@ -98,13 +91,10 @@ namespace AzToolsFramework
SourceControlCommandBus::Handler::BusDisconnect();
SourceControlConnectionRequestBus::Handler::BusDisconnect();
if (m_p4ApplicationDetected)
{
m_shutdownThreadSignal = true; // tell the thread to die.
m_WorkerSemaphore.release(1); // wake up the thread so that it sees the signal
m_WorkerThread.join(); // wait for the thread to finish.
m_WorkerThread = AZStd::thread();
}
m_shutdownThreadSignal = true; // tell the thread to die.
m_WorkerSemaphore.release(1); // wake up the thread so that it sees the signal
m_WorkerThread.join(); // wait for the thread to finish.
m_WorkerThread = AZStd::thread();
SetConnection(nullptr);
}

@ -260,7 +260,5 @@ namespace AzToolsFramework
AZStd::atomic_bool m_validConnection;
SourceControlState m_connectionState;
bool m_p4ApplicationDetected { false };
};
} // namespace AzToolsFramework

@ -177,4 +177,14 @@ namespace AzToolsFramework
DisplayQueuedNotification();
}
}
void ToastNotificationsView::SetOffset(const QPoint& offset)
{
m_offset = offset;
}
void ToastNotificationsView::SetAnchorPoint(const QPointF& anchorPoint)
{
m_anchorPoint = anchorPoint;
}
}

@ -50,6 +50,9 @@ namespace AzToolsFramework
void OnShow();
void UpdateToastPosition();
void SetOffset(const QPoint& offset);
void SetAnchorPoint(const QPointF& anchorPoint);
private:
ToastId CreateToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration);
void DisplayQueuedNotification();

@ -43,7 +43,7 @@ ly_add_target(
3rdParty::pybind11
AZ::AzCore
AZ::AzFramework
AZ::AzQtComponents
AZ::AzToolsFramework
)
ly_add_target(

@ -40,5 +40,6 @@
<file>Delete.svg</file>
<file>Download.svg</file>
<file>in_progress.gif</file>
<file>gem.svg</file>
</qresource>
</RCC>

@ -61,6 +61,24 @@ QTabBar::tab:focus {
color: #4082eb;
}
#ToastNotification {
background-color: black;
border-radius: 20px;
border:1px solid #dddddd;
qproperty-minimumSize: 100px 50px;
}
#ToastNotification #icon_frame {
border-radius: 4px;
qproperty-minimumSize: 44px 20px;
}
#ToastNotification #iconLabel {
qproperty-minimumSize: 30px 20px;
qproperty-maximumSize: 30px 20px;
margin-left: 6px;
}
/************** General (Forms) **************/
#formLineEditWidget,
@ -505,6 +523,14 @@ QProgressBar::chunk {
background-color: #444444;
}
#gemCatalogMenuButton {
qproperty-flat: true;
max-width:36px;
min-width:36px;
max-height:24px;
min-height:24px;
}
#GemCatalogHeaderLabel {
font-size: 12px;
color: #FFFFFF;

@ -0,0 +1,3 @@
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.3771 6.26808L18.0075 0.389195C17.9407 0.271396 17.844 0.173353 17.7271 0.105004C17.6101 0.0366557 17.4772 0.000430184 17.3418 0H4.15017C4.01474 0.000430184 3.88184 0.0366557 3.76492 0.105004C3.64801 0.173353 3.55125 0.271396 3.48444 0.389195L0.10459 6.26808C0.0213476 6.41083 -0.0136462 6.57661 0.00480427 6.74082C0.0232547 6.90502 0.0941655 7.05891 0.20701 7.17962L10.1724 17.7596C10.2442 17.8355 10.3308 17.896 10.4267 17.9373C10.5227 17.9787 10.6261 18 10.7306 18C10.8351 18 10.9385 17.9787 11.0345 17.9373C11.1305 17.896 11.217 17.8355 11.2888 17.7596L21.2542 7.17962C21.3703 7.06129 21.4451 6.90857 21.4672 6.74428C21.4894 6.57999 21.4578 6.41294 21.3771 6.26808ZM12.5998 7.17962L10.7562 13.7345L8.88195 7.17962H12.5998ZM8.42107 5.64332L7.25348 1.54654H14.218L13.0504 5.64332H8.42107ZM9.44526 14.687L2.31685 7.17962H7.24324L9.44526 14.687ZM14.2385 7.17962H19.1546L11.9853 14.7382L14.2385 7.17962ZM19.2878 5.64332H14.6789L15.8465 1.54654H16.9014L19.2878 5.64332ZM4.64178 1.54654H5.66598L6.83356 5.64332H2.23492L4.64178 1.54654Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -8,14 +8,13 @@
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <AzCore/std/functional.h>
#include <TagWidget.h>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QLabel>
#include <QPushButton>
#include <QMenu>
#include <QProgressBar>
#include <TagWidget.h>
#include <QMenu>
namespace O3DE::ProjectManager
{
@ -406,7 +405,6 @@ namespace O3DE::ProjectManager
CartButton* cartButton = new CartButton(gemModel, downloadController);
hLayout->addWidget(cartButton);
hLayout->addSpacing(16);
// Separating line
@ -418,9 +416,9 @@ namespace O3DE::ProjectManager
hLayout->addSpacing(16);
QMenu* gemMenu = new QMenu(this);
m_openGemReposAction = gemMenu->addAction(tr("Show Gem Repos"));
connect(m_openGemReposAction, &QAction::triggered, this,[this](){ emit OpenGemsRepo(); });
gemMenu->addAction( tr("Show Gem Repos"), [this]() { emit OpenGemsRepo(); });
gemMenu->addSeparator();
gemMenu->addAction( tr("Add Existing Gem"), [this]() { emit AddGem(); });
QPushButton* gemMenuButton = new QPushButton(this);
gemMenuButton->setObjectName("gemCatalogMenuButton");

@ -8,24 +8,23 @@
#pragma once
#include <AzCore/std/function/function_fwd.h>
#if !defined(Q_MOC_RUN)
#include <AzCore/std/function/function_fwd.h>
#include <AzQtComponents/Components/SearchLineEdit.h>
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <TagWidget.h>
#include <DownloadController.h>
#include <QFrame>
#include <QLabel>
#include <QDialog>
#include <QMoveEvent>
#include <QHideEvent>
#include <QVBoxLayout>
#include <QAction>
#include <DownloadController.h>
#endif
QT_FORWARD_DECLARE_CLASS(QPushButton)
QT_FORWARD_DECLARE_CLASS(QLabel)
QT_FORWARD_DECLARE_CLASS(QVBoxLayout)
QT_FORWARD_DECLARE_CLASS(QHBoxLayout)
QT_FORWARD_DECLARE_CLASS(QHideEvent)
QT_FORWARD_DECLARE_CLASS(QMoveEvent)
namespace O3DE::ProjectManager
{
class CartOverlayWidget
@ -87,12 +86,11 @@ namespace O3DE::ProjectManager
void ReinitForProject();
signals:
void AddGem();
void OpenGemsRepo();
private:
AzQtComponents::SearchLineEdit* m_filterLineEdit = nullptr;
inline constexpr static int s_height = 60;
QAction* m_openGemReposAction = nullptr;
};
} // namespace O3DE::ProjectManager

@ -19,6 +19,10 @@
#include <QTimer>
#include <PythonBindingsInterface.h>
#include <QMessageBox>
#include <QDir>
#include <QStandardPaths>
#include <QFileDialog>
#include <QMessageBox>
namespace O3DE::ProjectManager
{
@ -66,11 +70,15 @@ namespace O3DE::ProjectManager
hLayout->addWidget(filterWidget);
hLayout->addLayout(middleVLayout);
hLayout->addWidget(m_gemInspector);
m_notificationsView = AZStd::make_unique<AzToolsFramework::ToastNotificationsView>(this, AZ_CRC("GemCatalogNotificationsView"));
m_notificationsView->SetOffset(QPoint(10, 70));
}
void GemCatalogScreen::ReinitForProject(const QString& projectPath)
{
m_gemModel->clear();
m_gemsToRegisterWithProject.clear();
FillModel(projectPath);
if (m_filterWidget)
@ -86,6 +94,48 @@ namespace O3DE::ProjectManager
m_headerWidget->ReinitForProject();
connect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetGemStatusFilter);
connect(m_gemModel, &GemModel::gemStatusChanged, this, &GemCatalogScreen::OnGemStatusChanged);
connect(
m_headerWidget, &GemCatalogHeaderWidget::AddGem,
[&]()
{
EngineInfo engineInfo;
QString defaultPath;
AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
if (engineInfoResult.IsSuccess())
{
engineInfo = engineInfoResult.GetValue();
defaultPath = engineInfo.m_defaultGemsFolder;
}
if (defaultPath.isEmpty())
{
defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
}
QString directory = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(this, tr("Browse"), defaultPath));
if (!directory.isEmpty())
{
// register the gem to the o3de_manifest.json and to the project after the user confirms
// project creation/update
auto registerResult = PythonBindingsInterface::Get()->RegisterGem(directory);
if(!registerResult)
{
QMessageBox::critical(this, tr("Failed to add gem"), registerResult.GetError().c_str());
}
else
{
m_gemsToRegisterWithProject.insert(directory);
AZ::Outcome<GemInfo, void> gemInfoResult = PythonBindingsInterface::Get()->GetGemInfo(directory);
if (gemInfoResult)
{
m_gemModel->AddGem(gemInfoResult.GetValue<GemInfo>());
m_gemModel->UpdateGemDependencies();
}
}
}
});
// Select the first entry after everything got correctly sized
QTimer::singleShot(200, [=]{
@ -94,6 +144,72 @@ namespace O3DE::ProjectManager
});
}
void GemCatalogScreen::OnGemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies)
{
if (m_notificationsEnabled)
{
bool added = GemModel::IsAdded(modelIndex);
bool dependency = GemModel::IsAddedDependency(modelIndex);
bool gemStateChanged = (added && !dependency) || (!added && !dependency);
if (!gemStateChanged && !numChangedDependencies)
{
// no actual changes made
return;
}
QString notification;
if (gemStateChanged)
{
notification = GemModel::GetDisplayName(modelIndex);
if (numChangedDependencies > 0)
{
notification += " " + tr("and") + " ";
}
}
if (numChangedDependencies == 1 )
{
notification += "1 Gem " + tr("dependency");
}
else if (numChangedDependencies > 1)
{
notification += QString("%d Gem ").arg(numChangedDependencies) + tr("dependencies");
}
notification += " " + (added ? tr("activated") : tr("deactivated"));
AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Custom, notification, "");
toastConfiguration.m_customIconImage = ":/gem.svg";
toastConfiguration.m_borderRadius = 4;
toastConfiguration.m_duration = AZStd::chrono::milliseconds(3000);
m_notificationsView->ShowToastNotification(toastConfiguration);
}
}
void GemCatalogScreen::hideEvent(QHideEvent* event)
{
ScreenWidget::hideEvent(event);
m_notificationsView->OnHide();
}
void GemCatalogScreen::showEvent(QShowEvent* event)
{
ScreenWidget::showEvent(event);
m_notificationsView->OnShow();
}
void GemCatalogScreen::resizeEvent(QResizeEvent* event)
{
ScreenWidget::resizeEvent(event);
m_notificationsView->UpdateToastPosition();
}
void GemCatalogScreen::moveEvent(QMoveEvent* event)
{
ScreenWidget::moveEvent(event);
m_notificationsView->UpdateToastPosition();
}
void GemCatalogScreen::FillModel(const QString& projectPath)
{
AZ::Outcome<QVector<GemInfo>, AZStd::string> allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(projectPath);
@ -121,6 +237,7 @@ namespace O3DE::ProjectManager
}
m_gemModel->UpdateGemDependencies();
m_notificationsEnabled = false;
// Gather enabled gems for the given project.
auto enabledGemNamesResult = PythonBindingsInterface::Get()->GetEnabledGemNames(projectPath);
@ -147,6 +264,8 @@ namespace O3DE::ProjectManager
{
QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve enabled gems for project %1.<br><br>Error:<br>%2").arg(projectPath, enabledGemNamesResult.GetError().c_str()));
}
m_notificationsEnabled = true;
}
else
{
@ -192,6 +311,12 @@ namespace O3DE::ProjectManager
return EnableDisableGemsResult::Failed;
}
// register external gems that were added with relative paths
if (m_gemsToRegisterWithProject.contains(gemPath))
{
pythonBindings->RegisterGem(QDir(projectPath).relativeFilePath(gemPath), projectPath);
}
}
for (const QModelIndex& modelIndex : toBeRemoved)

@ -10,12 +10,16 @@
#if !defined(Q_MOC_RUN)
#include <ScreenWidget.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzToolsFramework/UI/Notifications/ToastNotificationsView.h>
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <GemCatalog/GemFilterWidget.h>
#include <GemCatalog/GemListView.h>
#include <GemCatalog/GemInspector.h>
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <QSet>
#include <QString>
#endif
namespace O3DE::ProjectManager
@ -41,13 +45,24 @@ namespace O3DE::ProjectManager
GemModel* GetGemModel() const { return m_gemModel; }
DownloadController* GetDownloadController() const { return m_downloadController; }
public slots:
void OnGemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies);
protected:
void hideEvent(QHideEvent* event) override;
void showEvent(QShowEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
void moveEvent(QMoveEvent* event) override;
private slots:
void HandleOpenGemRepo();
private:
private:
void FillModel(const QString& projectPath);
AZStd::unique_ptr<AzToolsFramework::ToastNotificationsView> m_notificationsView;
GemListView* m_gemListView = nullptr;
GemInspector* m_gemInspector = nullptr;
GemModel* m_gemModel = nullptr;
@ -56,5 +71,7 @@ namespace O3DE::ProjectManager
QVBoxLayout* m_filterWidgetLayout = nullptr;
GemFilterWidget* m_filterWidget = nullptr;
DownloadController* m_downloadController = nullptr;
bool m_notificationsEnabled = true;
QSet<QString> m_gemsToRegisterWithProject;
};
} // namespace O3DE::ProjectManager

@ -10,6 +10,7 @@
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <AzCore/Casting/numeric_cast.h>
#include <AzToolsFramework/UI/Notifications/ToastBus.h>
namespace O3DE::ProjectManager
{
@ -299,23 +300,50 @@ namespace O3DE::ProjectManager
AZ_Assert(gemModel, "Failed to obtain GemModel");
QVector<QModelIndex> dependencies = gemModel->GatherGemDependencies(modelIndex);
uint32_t numChangedDependencies = 0;
if (IsAdded(modelIndex))
{
for (const QModelIndex& dependency : dependencies)
{
SetIsAddedDependency(*gemModel, dependency, true);
if (!IsAddedDependency(dependency))
{
SetIsAddedDependency(*gemModel, dependency, true);
// if the gem was already added then the state didn't really change
if (!IsAdded(dependency))
{
numChangedDependencies++;
}
}
}
}
else
{
// still a dependency if some added gem depends on this one
SetIsAddedDependency(model, modelIndex, gemModel->HasDependentGems(modelIndex));
bool hasDependentGems = gemModel->HasDependentGems(modelIndex);
if (IsAddedDependency(modelIndex) != hasDependentGems)
{
SetIsAddedDependency(model, modelIndex, hasDependentGems);
}
for (const QModelIndex& dependency : dependencies)
{
SetIsAddedDependency(*gemModel, dependency, gemModel->HasDependentGems(dependency));
hasDependentGems = gemModel->HasDependentGems(dependency);
if (IsAddedDependency(dependency) != hasDependentGems)
{
SetIsAddedDependency(*gemModel, dependency, hasDependentGems);
// if the gem was already added then the state didn't really change
if (!IsAdded(dependency))
{
numChangedDependencies++;
}
}
}
}
gemModel->emit gemStatusChanged(modelIndex, numChangedDependencies);
}
void GemModel::SetIsAddedDependency(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded)
@ -488,5 +516,4 @@ namespace O3DE::ProjectManager
}
return result;
}
} // namespace O3DE::ProjectManager

@ -77,6 +77,9 @@ namespace O3DE::ProjectManager
int TotalAddedGems(bool includeDependencies = false) const;
signals:
void gemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies);
private:
void FindGemDisplayNamesByNameStrings(QStringList& inOutGemNames);
void GetAllDependingGems(const QModelIndex& modelIndex, QSet<QModelIndex>& inOutGems);

@ -557,6 +557,47 @@ namespace O3DE::ProjectManager
return AZ::Success(AZStd::move(gemNames));
}
AZ::Outcome<void, AZStd::string> PythonBindings::RegisterGem(const QString& gemPath, const QString& projectPath)
{
bool registrationResult = false;
auto result = ExecuteWithLockErrorHandling(
[&]
{
auto externalProjectPath = projectPath.isEmpty() ? pybind11::none() : QString_To_Py_Path(projectPath);
auto pythonRegistrationResult = m_register.attr("register")(
pybind11::none(), // engine_path
pybind11::none(), // project_path
QString_To_Py_Path(gemPath), // gem folder
pybind11::none(), // external subdirectory
pybind11::none(), // template_path
pybind11::none(), // restricted folder
pybind11::none(), // repo uri
pybind11::none(), // default_engines_folder
pybind11::none(), // default_projects_folder
pybind11::none(), // default_gems_folder
pybind11::none(), // default_templates_folder
pybind11::none(), // default_restricted_folder
pybind11::none(), // default_third_party_folder
pybind11::none(), // external_subdir_engine_path
externalProjectPath // external_subdir_project_path
);
// Returns an exit code so boolify it then invert result
registrationResult = !pythonRegistrationResult.cast<bool>();
});
if (!result.IsSuccess())
{
return AZ::Failure<AZStd::string>(result.GetError().c_str());
}
else if (!registrationResult)
{
return AZ::Failure<AZStd::string>(AZStd::string::format("Failed to register gem path %s", gemPath.toUtf8().constData()));
}
return AZ::Success();
}
bool PythonBindings::AddProject(const QString& path)
{
bool registrationResult = false;

@ -42,6 +42,7 @@ namespace O3DE::ProjectManager
AZ::Outcome<QVector<GemInfo>, AZStd::string> GetEngineGemInfos() override;
AZ::Outcome<QVector<GemInfo>, AZStd::string> GetAllGemInfos(const QString& projectPath) override;
AZ::Outcome<QVector<AZStd::string>, AZStd::string> GetEnabledGemNames(const QString& projectPath) override;
AZ::Outcome<void, AZStd::string> RegisterGem(const QString& gemPath, const QString& projectPath = {}) override;
// Project
AZ::Outcome<ProjectInfo> CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo) override;

@ -91,6 +91,14 @@ namespace O3DE::ProjectManager
*/
virtual AZ::Outcome<QVector<AZStd::string>, AZStd::string> GetEnabledGemNames(const QString& projectPath) = 0;
/**
* Registers the gem to the specified project, or to the o3de_manifest.json if no project path is given
* @param gemPath the path to the gem
* @param projectPath the path to the project. If empty, will register the external path in o3de_manifest.json
* @return An outcome with the success flag as well as an error message in case of a failure.
*/
virtual AZ::Outcome<void, AZStd::string> RegisterGem(const QString& gemPath, const QString& projectPath = {}) = 0;
// Projects

@ -54,6 +54,7 @@ VSOutput ShadowCatcherVS(VSInput IN)
DirectionalLightShadow::GetShadowCoords(
ViewSrg::m_shadowIndexDirectionalLight,
worldPosition,
OUT.m_worldNormal,
OUT.m_shadowCoords);
return OUT;

@ -112,13 +112,15 @@ VSOutput EnhancedPbr_ForwardPassVS(VSInput IN)
PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depth)
{
const float3 vertexNormal = normalize(IN.m_normal);
// ------- Tangents & Bitangets -------
float3 tangents[UvSetCount] = { IN.m_tangent.xyz, IN.m_tangent.xyz };
float3 bitangents[UvSetCount] = { IN.m_bitangent.xyz, IN.m_bitangent.xyz };
if ((o_parallax_feature_enabled && !o_enableSubsurfaceScattering) || o_normal_useTexture || (o_clearCoat_enabled && o_clearCoat_normal_useTexture) || o_detail_normal_useTexture)
{
PrepareGeneratedTangent(IN.m_normal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents);
PrepareGeneratedTangent(vertexNormal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents);
}
// ------- Depth & Parallax -------
@ -137,7 +139,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
float3x3 uvMatrix = MaterialSrg::m_parallaxUvIndex == 0 ? MaterialSrg::m_uvMatrix : CreateIdentity3x3();
float3x3 uvMatrixInverse = MaterialSrg::m_parallaxUvIndex == 0 ? MaterialSrg::m_uvMatrixInverse : CreateIdentity3x3();
GetParallaxInput(IN.m_normal, tangents[MaterialSrg::m_parallaxUvIndex], bitangents[MaterialSrg::m_parallaxUvIndex], MaterialSrg::m_heightmapScale, MaterialSrg::m_heightmapOffset,
GetParallaxInput(vertexNormal, tangents[MaterialSrg::m_parallaxUvIndex], bitangents[MaterialSrg::m_parallaxUvIndex], MaterialSrg::m_heightmapScale, MaterialSrg::m_heightmapOffset,
ObjectSrg::GetWorldMatrix(), uvMatrix, uvMatrixInverse,
IN.m_uv[MaterialSrg::m_parallaxUvIndex], IN.m_worldPosition, depth, IN.m_position.w, displacementIsClipped);
@ -150,7 +152,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight;
if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount)
{
DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, IN.m_shadowCoords);
DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, vertexNormal, IN.m_shadowCoords);
}
}
}
@ -185,7 +187,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
float2 normalUv = IN.m_uv[MaterialSrg::m_normalMapUvIndex];
float3x3 uvMatrix = MaterialSrg::m_normalMapUvIndex == 0 ? MaterialSrg::m_uvMatrix : CreateIdentity3x3(); // By design, only UV0 is allowed to apply transforms.
float detailLayerNormalFactor = MaterialSrg::m_detail_normal_factor * detailLayerBlendFactor;
surface.vertexNormal = normalize(IN.m_normal);
surface.vertexNormal = vertexNormal;
surface.normal = GetDetailedNormalInputWS(
isFrontFace, IN.m_normal,
tangents[MaterialSrg::m_normalMapUvIndex], bitangents[MaterialSrg::m_normalMapUvIndex], MaterialSrg::m_normalMap, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_normalFactor, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, uvMatrix, o_normal_useTexture,

@ -302,6 +302,7 @@ ProcessedMaterialInputs ProcessStandardMaterialInputs(StandardMaterialInputs inp
PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depthNDC)
{
const float3 vertexNormal = normalize(IN.m_normal);
depthNDC = IN.m_position.z;
s_blendMaskFromVertexStream = IN.m_blendMask;
@ -321,7 +322,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
|| o_layer3_o_clearCoat_normal_useTexture
)
{
PrepareGeneratedTangent(IN.m_normal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents);
PrepareGeneratedTangent(vertexNormal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents);
}
// ------- Debug Modes -------
@ -368,7 +369,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight;
if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount)
{
DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, IN.m_shadowCoords);
DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, vertexNormal, IN.m_shadowCoords);
}
}
}
@ -445,7 +446,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
normalTS = ReorientTangentSpaceNormal(normalTS, lightingInputLayer3.m_normalTS);
}
// [GFX TODO][ATOM-14591]: This will only work if the normal maps all use the same UV stream. We would need to add support for having them in different UV streams.
surface.vertexNormal = normalize(IN.m_normal);
surface.vertexNormal = vertexNormal;
surface.normal = normalize(TangentSpaceToWorld(normalTS, IN.m_normal, tangents[MaterialSrg::m_parallaxUvIndex], bitangents[MaterialSrg::m_parallaxUvIndex]));
// ------- Combine Albedo, roughness, specular, roughness ---------

@ -98,6 +98,8 @@ VSOutput StandardPbr_ForwardPassVS(VSInput IN)
PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depthNDC)
{
const float3 vertexNormal = normalize(IN.m_normal);
// ------- Tangents & Bitangets -------
float3 tangents[UvSetCount] = { IN.m_tangent.xyz, IN.m_tangent.xyz };
float3 bitangents[UvSetCount] = { IN.m_bitangent.xyz, IN.m_bitangent.xyz };
@ -128,7 +130,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight;
if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount)
{
DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, IN.m_shadowCoords);
DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, vertexNormal, IN.m_shadowCoords);
}
}
}
@ -146,7 +148,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
float2 normalUv = IN.m_uv[MaterialSrg::m_normalMapUvIndex];
float3x3 uvMatrix = MaterialSrg::m_normalMapUvIndex == 0 ? MaterialSrg::m_uvMatrix : CreateIdentity3x3(); // By design, only UV0 is allowed to apply transforms.
surface.vertexNormal = normalize(IN.m_normal);
surface.vertexNormal = vertexNormal;
surface.normal = GetNormalInputWS(MaterialSrg::m_normalMap, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, isFrontFace, IN.m_normal,
tangents[MaterialSrg::m_normalMapUvIndex], bitangents[MaterialSrg::m_normalMapUvIndex], uvMatrix, o_normal_useTexture, MaterialSrg::m_normalFactor);

@ -16,39 +16,6 @@
// licensed and released under Creative Commons 3.0 Attribution
// https://creativecommons.org/licenses/by/3.0/
float3 HueToRgb(float hue)
{
return saturate(float3(abs(hue * 6.0f - 3.0f) - 1.0f,
2.0f - abs(hue * 6.0f - 2.0f),
2.0f - abs(hue * 6.0f - 4.0f)));
}
float3 RgbToHcv(float3 rgb)
{
// Based on work by Sam Hocevar and Emil Persson
const float4 p = (rgb.g < rgb.b) ? float4(rgb.bg, -1.0f, 2.0f/3.0f) : float4(rgb.gb, 0.0f, -1.0f/3.0f);
const float4 q1 = (rgb.r < p.x) ? float4(p.xyw, rgb.r) : float4(rgb.r, p.yzx);
const float c = q1.x - min(q1.w, q1.y);
const float h = abs((q1.w - q1.y) / (6.0f * c + 0.000001f ) + q1.z);
return float3(h, c, q1.x);
}
float3 RgbToHsl(float3 rgb)
{
rgb.xyz = max(rgb.xyz, 0.000001f);
const float3 hcv = RgbToHcv(rgb);
const float L = hcv.z - hcv.y * 0.5f;
const float S = hcv.y / (1.0f - abs(L * 2.0f - 1.0f) + 0.000001f);
return float3(hcv.x, S, L);
}
float3 HslToRgb(float3 hsl)
{
const float3 rgb = HueToRgb(hsl.x);
const float c = (1.0f - abs(2.0f * hsl.z - 1.0f)) * hsl.y;
return (rgb - 0.5f) * c + hsl.z;
}
// Color temperature
float3 KelvinToRgb(float kelvin)
{

@ -66,12 +66,21 @@ float3 ColorGradeSaturation (float3 frameColor, float control)
return (frameColor - vLuminance) * control + vLuminance;
}
float3 ColorGradeKelvinColorTemp(float3 frameColor, float kelvin)
float3 ColorGradeWhiteBalance(float3 frameColor, float kelvin, float tint, float luminancePreservation)
{
const float3 kColor = TransformColor(KelvinToRgb(kelvin), ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
const float luminance = CalculateLuminance(frameColor, ColorSpaceId::ACEScg);
const float3 resHsl = RgbToHsl(frameColor.rgb * kColor.rgb); // Apply Kelvin color and convert to HSL
return HslToRgb(float3(resHsl.xy, luminance)); // Preserve luminance
// Apply Kelvin color and tint and calculate the new luminance
float3 adjustedColor = frameColor.rgb * kColor.rgb;
adjustedColor.g = max(0.0, adjustedColor.g + tint * 0.001);
const float adjustedLuminance = CalculateLuminance(adjustedColor, ColorSpaceId::ACEScg);
// Adjust the color based on the difference in luminance.
const float luminanceDifferenceRatio = luminance / adjustedLuminance;
const float3 adjustedColorLumPreserved = adjustedColor * luminanceDifferenceRatio;
return lerp(adjustedColor, adjustedColorLumPreserved, luminancePreservation);
}
// pow(f, e) won't work if f is negative, or may cause inf/NAN.
@ -132,7 +141,11 @@ float3 ColorGradeShadowsMidtonesHighlights (float3 frameColor, float shadowsStar
float3 ColorGrade(float3 frameColor)
{
frameColor = lerp(frameColor, ColorGradePostExposure(frameColor, PassSrg::m_colorGradingExposure), PassSrg::m_colorAdjustmentWeight);
frameColor = lerp(frameColor, ColorGradeKelvinColorTemp(frameColor, PassSrg::m_whiteBalanceKelvin), PassSrg::m_whiteBalanceWeight);
frameColor = lerp(frameColor, ColorGradeWhiteBalance(
frameColor, PassSrg::m_whiteBalanceKelvin,
PassSrg::m_whiteBalanceTint,
PassSrg::m_whiteBalanceLuminancePreservation),
PassSrg::m_whiteBalanceWeight);
frameColor = lerp(frameColor, ColorGradingContrast(frameColor, AcesCcMidGrey, PassSrg::m_colorGradingContrast), PassSrg::m_colorAdjustmentWeight);
frameColor = lerp(frameColor, ColorGradeColorFilter(frameColor, PassSrg::m_colorFilterSwatch.rgb,
PassSrg::m_colorFilterMultiply, PassSrg::m_colorFilterIntensity), PassSrg::m_colorAdjustmentWeight);

@ -14,6 +14,7 @@
#include "ShadowmapAtlasLib.azsli"
#include "BicubicPcfFilters.azsli"
#include "ReceiverPlaneDepthBias.azsli"
#include "NormalOffsetShadows.azsli"
// Before including this azsli file, a PassSrg must be defined with the following members:
// Texture2DArray<float> m_directionalLightShadowmap;
@ -45,6 +46,7 @@ class DirectionalLightShadow
static void GetShadowCoords(
uint lightIndex,
float3 worldPosition,
float3 worldNormal,
out float3 shadowCoords[ViewSrg::MaxCascadeCount]);
//! This calculates visibility ratio of the surface from the light origin.
@ -109,18 +111,22 @@ class DirectionalLightShadow
void DirectionalLightShadow::GetShadowCoords(
uint lightIndex,
float3 worldPosition,
float3 worldNormal,
out float3 shadowCoords[ViewSrg::MaxCascadeCount])
{
const uint cascadeCount = ViewSrg::m_directionalLightShadows[lightIndex].m_cascadeCount;
const float shadowBias = ViewSrg::m_directionalLightShadows[lightIndex].m_shadowBias;
const float4x4 lightViewToShadowmapMatrices[ViewSrg::MaxCascadeCount] = ViewSrg::m_directionalLightShadows[lightIndex].m_lightViewToShadowmapMatrices;
const float4x4 worldToLightViewMatrices[ViewSrg::MaxCascadeCount] = ViewSrg::m_directionalLightShadows[lightIndex].m_worldToLightViewMatrices;
const uint cascadeCount = ViewSrg::m_directionalLightShadows[lightIndex].m_cascadeCount;
const float3 shadowOffset = ComputeNormalShadowOffset(ViewSrg::m_directionalLightShadows[lightIndex].m_normalShadowBias, worldNormal, ViewSrg::m_directionalLightShadows[lightIndex].m_shadowmapSize);
for (uint index = 0; index < cascadeCount; ++index)
{
float4 lightSpacePos = mul(worldToLightViewMatrices[index], float4(worldPosition, 1.));
{
float4 lightSpacePos = mul(worldToLightViewMatrices[index], float4(worldPosition + shadowOffset, 1.));
lightSpacePos.z += shadowBias;
const float4 clipSpacePos = mul(lightViewToShadowmapMatrices[index], lightSpacePos);
shadowCoords[index] = clipSpacePos.xyz / clipSpacePos.w;
}

@ -0,0 +1,22 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
// Helper functions for normal offset shadow mapping.
// Normal Offset is an alternative to slope-scale depth bias.
// We bias the shadow map lookup by transforming the world-position along the geometric normal before hand.
// http://web.archive.org/web/20140810230446/https://www.dissidentlogic.com/old/#Normal%20Offset%20Shadows
//
// Apply the following to the world position. Then use this modified world position to look up in the shadow map
float3 ComputeNormalShadowOffset(const float normalOffsetBias, const float3 worldNormal, const float shadowMapDimension)
{
const float shadowmapSize = 2.0f / shadowMapDimension;
return float3(worldNormal * normalOffsetBias * shadowmapSize);
}

@ -47,6 +47,7 @@ void VertexHelper(in VSInput IN, inout VSOutput OUT, float3 worldPosition, bool
DirectionalLightShadow::GetShadowCoords(
shadowIndex,
worldPosition,
OUT.m_normal,
OUT.m_shadowCoords);
}
}

@ -81,10 +81,10 @@ partial ShaderResourceGroup ViewSrg
uint m_shadowmapArraySlice; // array slice who has shadowmap in the atlas.
uint m_shadowFilterMethod;
float m_boundaryScale;
uint m_predictionSampleCount;
uint m_filteringSampleCount;
float2 m_unprojectConstants;
float m_bias;
float m_normalShadowBias;
float m_esmExponent;
float3 m_padding;
};
@ -109,7 +109,7 @@ partial ShaderResourceGroup ViewSrg
uint m_shadowmapSize; // width and height of shadowmap
uint m_cascadeCount;
float m_shadowBias;
uint m_predictionSampleCount;
float m_normalShadowBias;
uint m_filteringSampleCount;
uint m_debugFlags;
uint m_shadowFilterMethod;

@ -57,6 +57,7 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback
float m_whiteBalanceWeight;
float m_whiteBalanceKelvin;
float m_whiteBalanceTint;
float m_whiteBalanceLuminancePreservation;
float m_splitToneBalance;
float m_splitToneWeight;

@ -40,6 +40,7 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback
float m_whiteBalanceWeight;
float m_whiteBalanceKelvin;
float m_whiteBalanceTint;
float m_whiteBalanceLuminancePreservation;
float m_splitToneBalance;
float m_splitToneWeight;

@ -160,6 +160,9 @@ namespace AZ
//! Reduces acne by applying a small amount of bias along shadow-space z.
virtual void SetShadowBias(LightHandle handle, float bias) = 0;
//! Reduces acne by biasing the shadowmap lookup along the geometric normal.
virtual void SetNormalShadowBias(LightHandle handle, float normalShadowBias) = 0;
};
} // namespace Render
} // namespace AZ

@ -22,6 +22,7 @@ AZ_GFX_VEC3_PARAM(ColorFilterSwatch, m_colorFilterSwatch, AZ::Vector3(1.0f, 0.5f
AZ_GFX_FLOAT_PARAM(WhiteBalanceWeight, m_whiteBalanceWeight, 0.0)
AZ_GFX_FLOAT_PARAM(WhiteBalanceKelvin, m_whiteBalanceKelvin, 6600.0)
AZ_GFX_FLOAT_PARAM(WhiteBalanceTint, m_whiteBalanceTint, 0.0)
AZ_GFX_FLOAT_PARAM(WhiteBalanceLuminancePreservation, m_whiteBalanceLuminancePreservation, 1.0)
AZ_GFX_FLOAT_PARAM(SplitToneWeight, m_splitToneWeight, 0.0)
AZ_GFX_FLOAT_PARAM(SplitToneBalance, m_splitToneBalance, 0.0)

@ -48,11 +48,13 @@ namespace AZ::Render
virtual void SetAspectRatio(ShadowId id, float aspectRatio) = 0;
//! Sets the field of view for the shadow in radians in the Y direction.
virtual void SetFieldOfViewY(ShadowId id, float fieldOfView) = 0;
//! Sets the maximum resolution of the shadow map
//! Sets the maximum resolution of the shadow map.
virtual void SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size) = 0;
//! Sets the shadow bias
//! Sets the shadow bias.
virtual void SetShadowBias(ShadowId id, float bias) = 0;
//! Sets the shadow filter method
//! Sets the normal shadow bias.
virtual void SetNormalShadowBias(ShadowId id, float normalShadowBias) = 0;
//! Sets the shadow filter method.
virtual void SetShadowFilterMethod(ShadowId id, ShadowFilterMethod method) = 0;
//! Sets the sample count for filtering of the shadow boundary, max 64.
virtual void SetFilteringSampleCount(ShadowId id, uint16_t count) = 0;

@ -598,6 +598,15 @@ namespace AZ
m_shadowBufferNeedsUpdate = true;
}
void DirectionalLightFeatureProcessor::SetNormalShadowBias(LightHandle handle, float normalShadowBias)
{
for (auto& it : m_shadowData)
{
it.second.GetData(handle.GetIndex()).m_normalShadowBias = normalShadowBias;
}
m_shadowBufferNeedsUpdate = true;
}
void DirectionalLightFeatureProcessor::OnRenderPipelineAdded(RPI::RenderPipelinePtr pipeline)
{
PrepareForChangingRenderPipelineAndCameraView();

@ -92,8 +92,9 @@ namespace AZ
uint32_t m_shadowmapSize = 1; // width and height of shadowmap
uint32_t m_cascadeCount = 1;
// Reduce acne by applying a small amount of bias to apply along shadow-space z.
float m_shadowBias = 0.0f;
uint32_t m_predictionSampleCount = 0;
float m_shadowBias = 0.0f;
// Reduces acne by biasing the shadowmap lookup along the geometric normal.
float m_normalShadowBias;
uint32_t m_filteringSampleCount = 0;
uint32_t m_debugFlags = 0;
uint32_t m_shadowFilterMethod = 0;
@ -101,6 +102,8 @@ namespace AZ
float m_padding[3];
};
static_assert(sizeof(DirectionalLightShadowData) % 16 == 0); // Structured buffers need alignment to be a multiple of 16 bytes.
class DirectionalLightFeatureProcessor final
: public DirectionalLightFeatureProcessorInterface
{
@ -216,6 +219,7 @@ namespace AZ
void SetFilteringSampleCount(LightHandle handle, uint16_t count) override;
void SetShadowReceiverPlaneBiasEnabled(LightHandle handle, bool enable) override;
void SetShadowBias(LightHandle handle, float bias) override;
void SetNormalShadowBias(LightHandle handle, float normalShadowBias) override;
const Data::Instance<RPI::Buffer> GetLightBuffer() const;
uint32_t GetLightCount() const;

@ -43,6 +43,7 @@
m_whiteBalanceWeightIndex.Reset();
m_whiteBalanceKelvinIndex.Reset();
m_whiteBalanceTintIndex.Reset();
m_whiteBalanceLuminancePreservationIndex.Reset();
m_splitToneBalanceIndex.Reset();
m_splitToneWeightIndex.Reset();
@ -96,7 +97,7 @@
m_shaderResourceGroup->SetConstant(m_whiteBalanceWeightIndex, settings->GetWhiteBalanceWeight());
m_shaderResourceGroup->SetConstant(m_whiteBalanceKelvinIndex, settings->GetWhiteBalanceKelvin());
m_shaderResourceGroup->SetConstant(m_whiteBalanceTintIndex, settings->GetWhiteBalanceTint());
m_shaderResourceGroup->SetConstant(m_whiteBalanceLuminancePreservationIndex, settings->GetWhiteBalanceLuminancePreservation());
m_shaderResourceGroup->SetConstant(m_splitToneBalanceIndex, settings->GetSplitToneBalance());
m_shaderResourceGroup->SetConstant(m_splitToneWeightIndex, settings->GetSplitToneWeight());
m_shaderResourceGroup->SetConstant(m_splitToneShadowsColorIndex, AZ::Vector4(settings->GetSplitToneShadowsColor()));

@ -55,6 +55,7 @@ namespace AZ
RHI::ShaderInputNameIndex m_whiteBalanceWeightIndex = "m_whiteBalanceWeight";
RHI::ShaderInputNameIndex m_whiteBalanceKelvinIndex = "m_whiteBalanceKelvin";
RHI::ShaderInputNameIndex m_whiteBalanceTintIndex = "m_whiteBalanceTint";
RHI::ShaderInputNameIndex m_whiteBalanceLuminancePreservationIndex = "m_whiteBalanceLuminancePreservation";
RHI::ShaderInputNameIndex m_splitToneBalanceIndex = "m_splitToneBalance";
RHI::ShaderInputNameIndex m_splitToneWeightIndex = "m_splitToneWeight";

@ -151,6 +151,14 @@ namespace AZ::Render
shadowProperty.m_bias = bias;
}
void ProjectedShadowFeatureProcessor::SetNormalShadowBias(ShadowId id, float normalShadowBias)
{
AZ_Assert(id.IsValid(), "Invalid ShadowId passed to ProjectedShadowFeatureProcessor::SetNormalShadowBias().");
ShadowProperty& shadowProperty = GetShadowPropertyFromShadowId(id);
shadowProperty.m_normalShadowBias = normalShadowBias;
}
void ProjectedShadowFeatureProcessor::SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size)
{
AZ_Assert(id.IsValid(), "Invalid ShadowId passed to ProjectedShadowFeatureProcessor::SetShadowmapMaxResolution().");

@ -48,6 +48,7 @@ namespace AZ::Render
void SetFieldOfViewY(ShadowId id, float fieldOfViewYRadians) override;
void SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size) override;
void SetShadowBias(ShadowId id, float bias) override;
void SetNormalShadowBias(ShadowId id, float normalShadowBias) override;
void SetShadowFilterMethod(ShadowId id, ShadowFilterMethod method) override;
void SetFilteringSampleCount(ShadowId id, uint16_t count) override;
void SetShadowProperties(ShadowId id, const ProjectedShadowDescriptor& descriptor) override;
@ -64,10 +65,10 @@ namespace AZ::Render
uint32_t m_shadowmapArraySlice = 0; // array slice who has shadowmap in the atlas.
uint32_t m_shadowFilterMethod = 0; // filtering method of shadows.
float m_boundaryScale = 0.f; // the half of boundary of lit/shadowed areas. (in degrees)
uint32_t m_predictionSampleCount = 0; // sample count to judge whether it is on the shadow boundary or not.
uint32_t m_filteringSampleCount = 0;
AZStd::array<float, 2> m_unprojectConstants = { {0, 0} };
float m_bias;
float m_normalShadowBias;
float m_esmExponent = 87.0f;
float m_padding[3];
};
@ -78,6 +79,7 @@ namespace AZ::Render
ProjectedShadowDescriptor m_desc;
RPI::ViewPtr m_shadowmapView;
float m_bias = 0.1f;
float m_normalShadowBias = 0.0f;
ShadowId m_shadowId;
};

@ -14,92 +14,86 @@ namespace AZ
{
namespace RHI
{
/**
* The platform-independent swap chain base class. Swap chains contain a "chain" of images which
* map to a platform-specific window, displayed on a physical monitor. The user is allowed
* to adjust the swap chain outside of the current FrameScheduler frame. Doing so within a frame scheduler
* frame results in undefined behavior.
*
* The frame scheduler controls presentation of the swap chain. The user may attach a swap chain to a scope
* in order to render to the current image.
*/
//! The platform-independent swap chain base class. Swap chains contain a "chain" of images which
//! map to a platform-specific window, displayed on a physical monitor. The user is allowed
//! to adjust the swap chain outside of the current FrameScheduler frame. Doing so within a frame scheduler
//! frame results in undefined behavior.
//!
//! The frame scheduler controls presentation of the swap chain. The user may attach a swap chain to a scope
//! in order to render to the current image.
class SwapChain
: public ImagePoolBase
{
public:
AZ_RTTI(SwapChain, "{888B64A5-D956-406F-9C33-CF6A54FC41B0}", Object);
virtual ~SwapChain();
/// Initializes the swap chain, making it ready for attachment.
//! Initializes the swap chain, making it ready for attachment.
ResultCode Init(RHI::Device& device, const SwapChainDescriptor& descriptor);
/// Presents the swap chain to the display, and rotates the images.
//! Presents the swap chain to the display, and rotates the images.
void Present();
/**
* Sets the vertical sync interval for the swap chain.
* 0 - No vsync.
* N - Sync to every N vertical refresh.
*
* A value of 1 syncs to the refresh rate of the monitor.
*/
//! Sets the vertical sync interval for the swap chain.
//! 0 - No vsync.
//! N - Sync to every N vertical refresh.
//!
//! A value of 1 syncs to the refresh rate of the monitor.
void SetVerticalSyncInterval(uint32_t verticalSyncInterval);
/**
* Resizes the display resolution of the swap chain. Ideally, this matches the platform window
* resolution. Typically, the resize operation will occur in reaction to a platform window size
* change. Takes effect immediately and results in a GPU pipeline flush.
*/
//! Resizes the display resolution of the swap chain. Ideally, this matches the platform window
//! resolution. Typically, the resize operation will occur in reaction to a platform window size
//! change. Takes effect immediately and results in a GPU pipeline flush.
ResultCode Resize(const SwapChainDimensions& dimensions);
/// Returns the number of images in the swap chain.
//! Returns the number of images in the swap chain.
uint32_t GetImageCount() const;
/// Returns the current image index of the swap chain.
//! Returns the current image index of the swap chain.
uint32_t GetCurrentImageIndex() const;
/// Returns the current image of the swap chain.
//! Returns the current image of the swap chain.
Image* GetCurrentImage() const;
/// Returns the image associated with the provided index, where the total number of images
/// is given by GetImageCount().
//! Returns the image associated with the provided index, where the total number of images
//! is given by GetImageCount().
Image* GetImage(uint32_t index) const;
/// Returns the ID used for the SwapChain's attachment
//! Returns the ID used for the SwapChain's attachment
const AttachmentId& GetAttachmentId() const;
/// Returns the descriptor provided when initializing the swap chain.
//! Returns the descriptor provided when initializing the swap chain.
const RHI::SwapChainDescriptor& GetDescriptor() const override final;
//! \return True if the swap chain prefers to use exclusive full screen mode.
//! Returns True if the swap chain prefers to use exclusive full screen mode.
virtual bool IsExclusiveFullScreenPreferred() const { return false; }
//! \return True if the swap chain prefers exclusive full screen mode and it is currently true, false otherwise.
//! Returns True if the swap chain prefers exclusive full screen mode and it is currently true, false otherwise.
virtual bool GetExclusiveFullScreenState() const { return false; }
//! \return True if the swap chain prefers exclusive full screen mode and a transition happened, false otherwise.
//! Return True if the swap chain prefers exclusive full screen mode and a transition happened, false otherwise.
virtual bool SetExclusiveFullScreenState([[maybe_unused]]bool fullScreenState) { return false; }
AZ_RTTI(SwapChain, "{888B64A5-D956-406F-9C33-CF6A54FC41B0}", Object);
protected:
SwapChain();
struct InitImageRequest
{
/// Pointer to the image to initialize.
//! Pointer to the image to initialize.
Image* m_image = nullptr;
/// Index of the image in the swap chain.
//! Index of the image in the swap chain.
uint32_t m_imageIndex = 0;
/// Descriptor for the image.
//! Descriptor for the image.
ImageDescriptor m_descriptor;
};
//////////////////////////////////////////////////////////////////////////
// ResourcePool Overrides
/// Called when the pool is shutting down.
//! Called when the pool is shutting down.
void ShutdownInternal() override;
//////////////////////////////////////////////////////////////////////////
@ -111,32 +105,29 @@ namespace AZ
//////////////////////////////////////////////////////////////////////////
// Platform API
/// Called when the swap chain is initializing.
//! Called when the swap chain is initializing.
virtual ResultCode InitInternal(RHI::Device& device, const SwapChainDescriptor& descriptor, SwapChainDimensions* nativeDimensions) = 0;
/// called when the swap chain is initializing an image.
//! called when the swap chain is initializing an image.
virtual ResultCode InitImageInternal(const InitImageRequest& request) = 0;
/// Called when the swap chain is resizing.
//! Called when the swap chain is resizing.
virtual ResultCode ResizeInternal(const SwapChainDimensions& dimensions, SwapChainDimensions* nativeDimensions) = 0;
/// Called when the swap chain is presenting the currently swap image.
/// Returns the index of the current image after the swap.
//! Called when the swap chain is presenting the currently swap image.
//! Returns the index of the current image after the swap.
virtual uint32_t PresentInternal() = 0;
virtual void SetVerticalSyncIntervalInternal(uint32_t previousVerticalSyncInterval)
{
AZ_UNUSED(previousVerticalSyncInterval);
}
virtual void SetVerticalSyncIntervalInternal([[maybe_unused]]uint32_t previousVerticalSyncInterval) {}
//////////////////////////////////////////////////////////////////////////
SwapChainDescriptor m_descriptor;
/// Images corresponding to each image in the swap chain.
//! Images corresponding to each image in the swap chain.
AZStd::vector<Ptr<Image>> m_images;
/// The current image index.
//! The current image index.
uint32_t m_currentImageIndex = 0;
};
}

@ -55,7 +55,7 @@ namespace AZ
if (resultCode == ResultCode::Success)
{
m_descriptor = descriptor;
// Ovewrite descriptor dimensions with the native ones (the ones assigned by the platform) returned by InitInternal.
// Overwrite descriptor dimensions with the native ones (the ones assigned by the platform) returned by InitInternal.
m_descriptor.m_dimensions = nativeDimensions;
m_images.reserve(m_descriptor.m_dimensions.m_imageCount);
@ -129,8 +129,8 @@ namespace AZ
while (m_images.size() > static_cast<size_t>(m_descriptor.m_dimensions.m_imageCount))
{
m_images.pop_back();
}
}
InitImageRequest request;
RHI::ImageDescriptor& imageDescriptor = request.m_descriptor;

@ -27,6 +27,7 @@ namespace AZ
{
EndInternal();
m_device->GetCommandQueueContext().GetCommandQueue(m_hardwareQueueClass).ExecuteWork(AZStd::move(m_workRequest));
m_isExecuted = true;
}
bool FrameGraphExecuteGroupHandlerBase::IsComplete() const
@ -42,6 +43,11 @@ namespace AZ
return true;
}
bool FrameGraphExecuteGroupHandlerBase::IsExecuted() const
{
return m_isExecuted;
}
template<class T>
void InsertWorkRequestElements(T& destination, const T& source)
{

@ -41,6 +41,7 @@ namespace AZ
void End();
bool IsComplete() const;
bool IsExecuted() const;
protected:
virtual RHI::ResultCode InitInternal(Device& device, const AZStd::vector<RHI::FrameGraphExecuteGroup*>& executeGroups) = 0;
@ -52,6 +53,7 @@ namespace AZ
ExecuteWorkRequest m_workRequest;
RHI::HardwareQueueClass m_hardwareQueueClass = RHI::HardwareQueueClass::Graphics;
AZStd::vector<RHI::FrameGraphExecuteGroup*> m_executeGroups;
bool m_isExecuted = false;
};
}
}

@ -177,8 +177,8 @@ namespace AZ
auto findIter = m_groupHandlers.find(group.GetGroupId());
AZ_Assert(findIter != m_groupHandlers.end(), "Could not find group handler for groupId %d", group.GetGroupId().GetIndex());
FrameGraphExecuteGroupHandlerBase* handler = findIter->second.get();
// Wait until all execute groups of the handler has finished.
if (handler->IsComplete())
// Wait until all execute groups of the handler has finished and also make sure that the handler itself hasn't executed already (which is possible for parallel encoding).
if (!handler->IsExecuted() && handler->IsComplete())
{
// This will execute the recorded work into the queue.
handler->End();

@ -61,13 +61,12 @@ namespace AZ
void SwapChain::SetVerticalSyncIntervalInternal(uint32_t previousVsyncInterval)
{
uint32_t verticalSyncInterval = GetDescriptor().m_verticalSyncInterval;
if (verticalSyncInterval == 0 || previousVsyncInterval == 0)
if (GetDescriptor().m_verticalSyncInterval == 0 || previousVsyncInterval == 0)
{
// The presentation mode may change when transitioning to or from a vsynced presentation mode
// In this case, the swapchain must be recreated.
InvalidateNativeSwapChain();
BuildNativeSwapChain(GetDescriptor().m_dimensions, verticalSyncInterval);
CreateSwapchain();
}
}
@ -85,46 +84,21 @@ namespace AZ
RHI::DeviceObject::Init(baseDevice);
auto& device = static_cast<Device&>(GetDevice());
RHI::SwapChainDimensions swapchainDimensions = descriptor.m_dimensions;
m_dimensions = descriptor.m_dimensions;
result = BuildSurface(descriptor);
RETURN_RESULT_IF_UNSUCCESSFUL(result);
if (!ValidateSurfaceDimensions(swapchainDimensions))
{
swapchainDimensions.m_imageHeight = AZStd::clamp(swapchainDimensions.m_imageHeight, m_surfaceCapabilities.minImageExtent.height, m_surfaceCapabilities.maxImageExtent.height);
swapchainDimensions.m_imageWidth = AZStd::clamp(swapchainDimensions.m_imageWidth, m_surfaceCapabilities.minImageExtent.width, m_surfaceCapabilities.maxImageExtent.width);
AZ_Printf("Vulkan", "Resizing swapchain from (%d, %d) to (%d, %d).",
static_cast<int>(descriptor.m_dimensions.m_imageWidth), static_cast<int>(descriptor.m_dimensions.m_imageHeight),
static_cast<int>(swapchainDimensions.m_imageWidth), static_cast<int>(swapchainDimensions.m_imageHeight));
}
auto& presentationQueue = device.GetCommandQueueContext().GetOrCreatePresentationCommandQueue(*this);
m_presentationQueue = &presentationQueue;
result = BuildNativeSwapChain(swapchainDimensions, descriptor.m_verticalSyncInterval);
RETURN_RESULT_IF_UNSUCCESSFUL(result);
uint32_t imageCount = 0;
VkResult vkResult = vkGetSwapchainImagesKHR(device.GetNativeDevice(), m_nativeSwapChain, &imageCount, nullptr);
AssertSuccess(vkResult);
RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult));
m_swapchainNativeImages.resize(imageCount);
// Retrieve the native images of the swapchain so they are
// available when we init the Images in InitImageInternal
vkResult = vkGetSwapchainImagesKHR(device.GetNativeDevice(), m_nativeSwapChain, &imageCount, m_swapchainNativeImages.data());
AssertSuccess(vkResult);
RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult));
// Acquire the first image
uint32_t imageIndex = 0;
result = AcquireNewImage(&imageIndex);
result = CreateSwapchain();
RETURN_RESULT_IF_UNSUCCESSFUL(result);
if (nativeDimensions)
{
// Fill out the real swapchain dimensions to return
nativeDimensions->m_imageCount = imageCount;
nativeDimensions->m_imageHeight = swapchainDimensions.m_imageHeight;
nativeDimensions->m_imageWidth = swapchainDimensions.m_imageWidth;
*nativeDimensions = m_dimensions;
nativeDimensions->m_imageFormat = ConvertFormat(m_surfaceFormat.format);
}
@ -165,51 +139,24 @@ namespace AZ
RHI::ResultCode SwapChain::ResizeInternal(const RHI::SwapChainDimensions& dimensions, RHI::SwapChainDimensions* nativeDimensions)
{
auto& device = static_cast<Device&>(GetDevice());
m_dimensions = dimensions;
InvalidateNativeSwapChain();
InvalidateSurface();
RHI::SwapChainDimensions resizeDimensions = dimensions;
BuildSurface(GetDescriptor());
if (!ValidateSurfaceDimensions(dimensions))
{
resizeDimensions.m_imageHeight = AZStd::clamp(dimensions.m_imageHeight, m_surfaceCapabilities.minImageExtent.height, m_surfaceCapabilities.maxImageExtent.height);
resizeDimensions.m_imageWidth = AZStd::clamp(dimensions.m_imageWidth, m_surfaceCapabilities.minImageExtent.width, m_surfaceCapabilities.maxImageExtent.width);
AZ_Printf("Vulkan", "Resizing swapchain from (%d, %d) to (%d, %d).",
static_cast<int>(dimensions.m_imageWidth), static_cast<int>(dimensions.m_imageHeight),
static_cast<int>(resizeDimensions.m_imageWidth), static_cast<int>(resizeDimensions.m_imageHeight));
}
auto& presentationQueue = device.GetCommandQueueContext().GetOrCreatePresentationCommandQueue(*this);
m_presentationQueue = &presentationQueue;
BuildNativeSwapChain(resizeDimensions, GetDescriptor().m_verticalSyncInterval);
resizeDimensions.m_imageCount = 0;
VkResult vkResult = vkGetSwapchainImagesKHR(device.GetNativeDevice(), m_nativeSwapChain, &resizeDimensions.m_imageCount, nullptr);
RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult));
m_swapchainNativeImages.resize(resizeDimensions.m_imageCount);
// Retrieve the native images of the swapchain so they are
// available when we init the Images in InitImageInternal
vkResult = vkGetSwapchainImagesKHR(device.GetNativeDevice(), m_nativeSwapChain, &resizeDimensions.m_imageCount, m_swapchainNativeImages.data());
RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult));
// Do not recycle the semaphore because they may not ever get signaled and since
// we can't recycle Vulkan semaphores we just delete them.
m_currentFrameContext.m_imageAvailableSemaphore->SetRecycleValue(false);
m_currentFrameContext.m_presentableSemaphore->SetRecycleValue(false);
// Acquire the first image
uint32_t imageIndex = 0;
AcquireNewImage(&imageIndex);
CreateSwapchain();
if (nativeDimensions)
{
*nativeDimensions = resizeDimensions;
*nativeDimensions = m_dimensions;
// [ATOM-4840] This is a workaround when the windows is minimized (0x0 size).
// Add proper support to handle this case.
nativeDimensions->m_imageHeight = AZStd::max(resizeDimensions.m_imageHeight, 1u);
nativeDimensions->m_imageWidth = AZStd::max(resizeDimensions.m_imageWidth, 1u);
nativeDimensions->m_imageHeight = AZStd::max(m_dimensions.m_imageHeight, 1u);
nativeDimensions->m_imageWidth = AZStd::max(m_dimensions.m_imageWidth, 1u);
nativeDimensions->m_imageFormat = ConvertFormat(m_surfaceFormat.format);
}
return RHI::ResultCode::Success;
@ -271,20 +218,48 @@ namespace AZ
info.pImageIndices = &imageIndex;
info.pResults = nullptr;
[[maybe_unused]] const VkResult result = vkQueuePresentKHR(vulkanQueue->GetNativeQueue(), &info);
// Resizing window cause recreation of SwapChain after calling this method,
// so VK_SUBOPTIMAL_KHR or VK_ERROR_OUT_OF_DATE_KHR should not happen at this point.
AZ_Assert(result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR, "Failed to present swapchain %s", GetName().GetCStr());
AZ_Warning("Vulkan", result != VK_SUBOPTIMAL_KHR, "Suboptimal presentation of swapchain %s", GetName().GetCStr());
const VkResult result = vkQueuePresentKHR(vulkanQueue->GetNativeQueue(), &info);
// Vulkan's definition of the two types of errors.
// VK_ERROR_OUT_OF_DATE_KHR: "A surface has changed in such a way that it is no longer compatible with the swapchain,
// and further presentation requests using the swapchain will fail. Applications must query the new surface
// properties and recreate their swapchain if they wish to continue presenting to the surface."
// VK_SUBOPTIMAL_KHR: "A swapchain no longer matches the surface properties exactly, but can still be used to
// present to the surface successfully."
//
// These result values may occur after resizing or some window operation. We should update the surface info and recreate the swapchain.
// VK_SUBOPTIMAL_KHR is treated as success, but we better update the surface info as well.
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR)
{
InvalidateNativeSwapChain();
CreateSwapchain();
}
else
{
// Other errors are:
// VK_ERROR_OUT_OF_HOST_MEMORY
// VK_ERROR_OUT_OF_DEVICE_MEMORY
// VK_ERROR_DEVICE_LOST
// VK_ERROR_SURFACE_LOST_KHR
// VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT
AZ_Assert(result == VK_SUCCESS, "Unhandled error for swapchain presentation.");
}
};
m_presentationQueue->QueueCommand(AZStd::move(presentCommand));
uint32_t acquiredImageIndex = GetCurrentImageIndex();
AcquireNewImage(&acquiredImageIndex);
return acquiredImageIndex;
RHI::ResultCode result = AcquireNewImage(&acquiredImageIndex);
if (result == RHI::ResultCode::Fail)
{
InvalidateNativeSwapChain();
CreateSwapchain();
return 0;
}
else
{
return acquiredImageIndex;
}
}
RHI::ResultCode SwapChain::BuildSurface(const RHI::SwapChainDescriptor& descriptor)
@ -293,15 +268,8 @@ namespace AZ
surfaceDesc.m_windowHandle = descriptor.m_window;
RHI::Ptr<WSISurface> surface = WSISurface::Create();
const RHI::ResultCode result = surface->Init(surfaceDesc);
if (result == RHI::ResultCode::Success)
{
m_surface = surface;
auto& device = static_cast<Device&>(GetDevice());
const auto& physicalDevice = static_cast<const PhysicalDevice&>(device.GetPhysicalDevice());
VkResult vkResult = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice.GetNativePhysicalDevice(), m_surface->GetNativeSurface(), &m_surfaceCapabilities);
AssertSuccess(vkResult);
RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult));
}
RETURN_RESULT_IF_UNSUCCESSFUL(result);
m_surface = surface;
return result;
}
@ -373,6 +341,21 @@ namespace AZ
return supportedModes[0];
}
VkSurfaceCapabilitiesKHR SwapChain::GetSurfaceCapabilities()
{
AZ_Assert(m_surface, "Surface has not been initialized.");
auto& device = static_cast<Device&>(GetDevice());
const auto& physicalDevice = static_cast<const PhysicalDevice&>(device.GetPhysicalDevice());
VkSurfaceCapabilitiesKHR surfaceCapabilities;
VkResult vkResult = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(
physicalDevice.GetNativePhysicalDevice(), m_surface->GetNativeSurface(), &surfaceCapabilities);
AssertSuccess(vkResult);
return surfaceCapabilities;
}
VkCompositeAlphaFlagBitsKHR SwapChain::GetSupportedCompositeAlpha() const
{
VkFlags supportedModesBits = m_surfaceCapabilities.supportedCompositeAlpha;
@ -394,15 +377,9 @@ namespace AZ
return VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
}
RHI::ResultCode SwapChain::BuildNativeSwapChain(const RHI::SwapChainDimensions& dimensions, uint32_t verticalSyncInterval)
RHI::ResultCode SwapChain::BuildNativeSwapChain(const RHI::SwapChainDimensions& dimensions)
{
AZ_Assert(m_nativeSwapChain == VK_NULL_HANDLE, "Vulkan's native SwapChain has been initialized already.");
auto& device = static_cast<Device&>(GetDevice());
auto& queueContext = device.GetCommandQueueContext();
const VkExtent2D extent = {
dimensions.m_imageWidth,
dimensions.m_imageHeight
};
AZ_Assert(m_surface, "Surface is null.");
if (!ValidateSurfaceDimensions(dimensions))
@ -410,7 +387,11 @@ namespace AZ
AZ_Assert(false, "Swapchain dimensions are not supported.");
return RHI::ResultCode::InvalidArgument;
}
m_surfaceFormat = GetSupportedSurfaceFormat(dimensions.m_imageFormat);
auto& device = static_cast<Vulkan::Device&>(GetDevice());
auto& queueContext = device.GetCommandQueueContext();
const VkExtent2D extent = { dimensions.m_imageWidth, dimensions.m_imageHeight };
// If the graphic queue is the same as the presentation queue, then we will always acquire
// 1 image at the same time. If it's another queue, we will have 2 at the same time (while the other queue
// presents the image)
@ -441,11 +422,11 @@ namespace AZ
createInfo.imageArrayLayers = 1; // non-stereoscopic
createInfo.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
createInfo.queueFamilyIndexCount = static_cast<uint32_t>(familyIndices.size());
createInfo.queueFamilyIndexCount = aznumeric_cast<uint32_t>(familyIndices.size());
createInfo.pQueueFamilyIndices = familyIndices.empty() ? nullptr : familyIndices.data();
createInfo.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
createInfo.compositeAlpha = GetSupportedCompositeAlpha();
createInfo.presentMode = GetSupportedPresentMode(verticalSyncInterval);
createInfo.compositeAlpha = m_compositeAlphaFlagBits;
createInfo.presentMode = m_presentMode;
createInfo.clipped = VK_FALSE;
createInfo.oldSwapchain = VK_NULL_HANDLE;
@ -467,9 +448,6 @@ namespace AZ
VK_NULL_HANDLE,
acquiredImageIndex);
// Resizing window cause recreation of SwapChain before calling this method,
// so VK_SUBOPTIMAL_KHR or VK_ERROR_OUT_OF_DATE_KHR should not happen.
AssertSuccess(vkResult);
RHI::ResultCode result = ConvertResult(vkResult);
RETURN_RESULT_IF_UNSUCCESSFUL(result);
@ -484,6 +462,7 @@ namespace AZ
}
m_currentFrameContext.m_imageAvailableSemaphore = imageAvailableSemaphore;
m_currentFrameContext.m_presentableSemaphore = semaphoreAllocator.Allocate();
return result;
}
@ -502,5 +481,70 @@ namespace AZ
m_nativeSwapChain = VK_NULL_HANDLE;
}
}
RHI::ResultCode SwapChain::CreateSwapchain()
{
auto& device = static_cast<Device&>(GetDevice());
m_surfaceCapabilities = GetSurfaceCapabilities();
m_surfaceFormat = GetSupportedSurfaceFormat(GetDescriptor().m_dimensions.m_imageFormat);
m_presentMode = GetSupportedPresentMode(GetDescriptor().m_verticalSyncInterval);
m_compositeAlphaFlagBits = GetSupportedCompositeAlpha();
if (!ValidateSurfaceDimensions(m_dimensions))
{
uint32_t oldHeight = m_dimensions.m_imageHeight;
uint32_t oldWidth = m_dimensions.m_imageWidth;
m_dimensions.m_imageHeight = AZStd::clamp(
m_dimensions.m_imageHeight,
m_surfaceCapabilities.minImageExtent.height,
m_surfaceCapabilities.maxImageExtent.height);
m_dimensions.m_imageWidth = AZStd::clamp(
m_dimensions.m_imageWidth,
m_surfaceCapabilities.minImageExtent.width,
m_surfaceCapabilities.maxImageExtent.width);
AZ_Printf(
"Vulkan", "Resizing swapchain from (%u, %u) to (%u, %u).",
oldWidth, oldHeight, m_dimensions.m_imageWidth, m_dimensions.m_imageHeight);
}
RHI::ResultCode result = BuildNativeSwapChain(m_dimensions);
RETURN_RESULT_IF_UNSUCCESSFUL(result);
AZ_TracePrintf("Swapchain", "Swapchain created. Width: %u, Height: %u.", m_dimensions.m_imageWidth, m_dimensions.m_imageHeight);
// Do not recycle the semaphore because they may not ever get signaled and since
// we can't recycle Vulkan semaphores we just delete them.
if (m_currentFrameContext.m_imageAvailableSemaphore)
{
m_currentFrameContext.m_imageAvailableSemaphore->SetRecycleValue(false);
}
if (m_currentFrameContext.m_presentableSemaphore)
{
m_currentFrameContext.m_presentableSemaphore->SetRecycleValue(false);
}
m_dimensions.m_imageCount = 0;
VkResult vkResult = vkGetSwapchainImagesKHR(device.GetNativeDevice(), m_nativeSwapChain, &m_dimensions.m_imageCount, nullptr);
AssertSuccess(vkResult);
RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult));
m_swapchainNativeImages.resize(m_dimensions.m_imageCount);
// Retrieve the native images of the swapchain so they are
// available when we init the images in InitImageInternal
vkResult = vkGetSwapchainImagesKHR(
device.GetNativeDevice(), m_nativeSwapChain, &m_dimensions.m_imageCount, m_swapchainNativeImages.data());
AssertSuccess(vkResult);
RETURN_RESULT_IF_UNSUCCESSFUL(ConvertResult(vkResult));
AZ_TracePrintf("Swapchain", "Obtained presentable images.");
// Acquire the first image
uint32_t imageIndex = 0;
result = AcquireNewImage(&imageIndex);
RETURN_RESULT_IF_UNSUCCESSFUL(result);
AZ_TracePrintf("Swapchain", "Acquired the first image.");
return RHI::ResultCode::Success;
}
}
}

@ -70,24 +70,48 @@ namespace AZ
//////////////////////////////////////////////////////////////////////
RHI::ResultCode BuildSurface(const RHI::SwapChainDescriptor& descriptor);
//! Returns true is the swapchain dimensions are supported by the current surface.
bool ValidateSurfaceDimensions(const RHI::SwapChainDimensions& dimensions);
//! Returns the corresponding Vulkan format that is supported by the surface.
//! If such format is not found, return the first supported format from the surface.
VkSurfaceFormatKHR GetSupportedSurfaceFormat(const RHI::Format format) const;
//! Returns the correct presentation mode.
//! If verticalSyncInterval is non-zero, returns VK_PRESENT_MODE_FIFO_KHR.
//! Otherwise, choose preferred mode if they are supported.
//! If not, the first supported present mode is returned.
VkPresentModeKHR GetSupportedPresentMode(uint32_t verticalSyncInterval) const;
//! Returns the preferred alpha compositing modes if they are supported.
//! If not, error will be reported.
VkCompositeAlphaFlagBitsKHR GetSupportedCompositeAlpha() const;
RHI::ResultCode BuildNativeSwapChain(const RHI::SwapChainDimensions& dimensions, uint32_t verticalSyncInterval);
//! Returns the current surface capabilities.
VkSurfaceCapabilitiesKHR GetSurfaceCapabilities();
//! Create the swapchain when initializing, or
//! swapchain is no longer compatible or is sub-optimal with the surface.
RHI::ResultCode CreateSwapchain();
//! Build underlying Vulkan swapchain.
RHI::ResultCode BuildNativeSwapChain(const RHI::SwapChainDimensions& dimensions);
//! Retrieve the index of the next available presentable image.
RHI::ResultCode AcquireNewImage(uint32_t* acquiredImageIndex);
//! Destroy the surface.
void InvalidateSurface();
//! Destroy the old swapchain.
void InvalidateNativeSwapChain();
VkSwapchainKHR m_nativeSwapChain = VK_NULL_HANDLE;
RHI::Ptr<WSISurface> m_surface;
VkSwapchainKHR m_nativeSwapChain = VK_NULL_HANDLE;
CommandQueue* m_presentationQueue = nullptr;
VkSurfaceFormatKHR m_surfaceFormat = {};
VkSurfaceCapabilitiesKHR m_surfaceCapabilities;
FrameContext m_currentFrameContext;
//! Swapchain data
VkSurfaceFormatKHR m_surfaceFormat = {};
VkSurfaceCapabilitiesKHR m_surfaceCapabilities = {};
VkPresentModeKHR m_presentMode = {};
VkCompositeAlphaFlagBitsKHR m_compositeAlphaFlagBits = {};
AZStd::vector<VkImage> m_swapchainNativeImages;
RHI::SwapChainDimensions m_dimensions;
struct SwapChainBarrier
{
VkPipelineStageFlags m_srcPipelineStages = 0;
@ -95,8 +119,6 @@ namespace AZ
VkImageMemoryBarrier m_barrier = {};
bool m_isValid = false;
} m_swapChainBarrier;
AZStd::vector<VkImage> m_swapchainNativeImages;
};
}
}

@ -36,6 +36,8 @@
#include <AzToolsFramework/UI/UICore/QTreeViewStateSaver.hxx>
#include <AzToolsFramework/UI/UICore/QWidgetSavedState.h>
#include "AtomToolsFramework_Traits_Platform.h"
AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
#include <QMessageBox>
#include <QObject>
@ -217,7 +219,7 @@ namespace AtomToolsFramework
AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::StartDisconnectingAssetProcessor);
#if AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY
_exit(0);
::_exit(0);
#else
Base::Destroy();
#endif

@ -196,12 +196,8 @@ namespace AtomToolsFramework
bool RenderViewportWidget::event(QEvent* event)
{
// On some types of QEvents, a resize event is needed to make sure that the current viewport window
// needs to be updated based on a potential new surface dimensions.
switch (event->type())
{
case QEvent::ScreenChangeInternal:
case QEvent::UpdateLater:
case QEvent::Resize:
SendWindowResizeEvent();
break;

@ -176,6 +176,14 @@ namespace AZ
//! Shadow bias reduces acne by applying a small amount of offset along shadow-space z.
//! @param Sets the amount of bias to apply.
virtual void SetShadowBias(float bias) = 0;
//! Reduces acne by biasing the shadowmap lookup along the geometric normal.
//! @return Returns the amount of bias to apply.
virtual float GetNormalShadowBias() const = 0;
//! Reduces acne by biasing the shadowmap lookup along the geometric normal.
//! @param normalShadowBias Sets the amount of normal shadow bias to apply.
virtual void SetNormalShadowBias(float normalShadowBias) = 0;
};
using DirectionalLightRequestBus = EBus<DirectionalLightRequests>;

@ -101,6 +101,9 @@ namespace AZ
//! Method of shadow's filtering.
ShadowFilterMethod m_shadowFilterMethod = ShadowFilterMethod::None;
// Reduces acne by biasing the shadowmap lookup along the geometric normal.
float m_normalShadowBias = 0.0f;
//! Sample Count for filtering (from 4 to 64)
//! It is used only when the pixel is predicted as on the boundary.
uint16_t m_filteringSampleCount = 32;

@ -39,7 +39,8 @@ namespace AZ
->Field("ShadowFilterMethod", &DirectionalLightComponentConfig::m_shadowFilterMethod)
->Field("PcfFilteringSampleCount", &DirectionalLightComponentConfig::m_filteringSampleCount)
->Field("ShadowReceiverPlaneBiasEnabled", &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled)
->Field("Shadow Bias", &DirectionalLightComponentConfig::m_shadowBias);
->Field("Shadow Bias", &DirectionalLightComponentConfig::m_shadowBias)
->Field("Normal Shadow Bias", &DirectionalLightComponentConfig::m_normalShadowBias);
}
}

@ -86,6 +86,8 @@ namespace AZ
->Event("SetShadowReceiverPlaneBiasEnabled", &DirectionalLightRequestBus::Events::SetShadowReceiverPlaneBiasEnabled)
->Event("GetShadowBias", &DirectionalLightRequestBus::Events::GetShadowBias)
->Event("SetShadowBias", &DirectionalLightRequestBus::Events::SetShadowBias)
->Event("GetNormalShadowBias", &DirectionalLightRequestBus::Events::GetNormalShadowBias)
->Event("SetNormalShadowBias", &DirectionalLightRequestBus::Events::SetNormalShadowBias)
->VirtualProperty("Color", "GetColor", "SetColor")
->VirtualProperty("Intensity", "GetIntensity", "SetIntensity")
->VirtualProperty("AngularDiameter", "GetAngularDiameter", "SetAngularDiameter")
@ -101,7 +103,8 @@ namespace AZ
->VirtualProperty("ShadowFilterMethod", "GetShadowFilterMethod", "SetShadowFilterMethod")
->VirtualProperty("FilteringSampleCount", "GetFilteringSampleCount", "SetFilteringSampleCount")
->VirtualProperty("ShadowReceiverPlaneBiasEnabled", "GetShadowReceiverPlaneBiasEnabled", "SetShadowReceiverPlaneBiasEnabled")
->VirtualProperty("ShadowBias", "GetShadowBias", "SetShadowBias");
->VirtualProperty("ShadowBias", "GetShadowBias", "SetShadowBias")
->VirtualProperty("NormalShadowBias", "GetNormalShadowBias", "SetNormalShadowBias");
;
}
}
@ -423,6 +426,20 @@ namespace AZ
return m_configuration.m_shadowBias;
}
void DirectionalLightComponentController::SetNormalShadowBias(float bias)
{
m_configuration.m_normalShadowBias = bias;
if (m_featureProcessor)
{
m_featureProcessor->SetNormalShadowBias(m_lightHandle, bias);
}
}
float DirectionalLightComponentController::GetNormalShadowBias() const
{
return m_configuration.m_normalShadowBias;
}
void DirectionalLightComponentController::SetFilteringSampleCount(uint32_t count)
{
const uint16_t count16 = GetMin(Shadow::MaxPcfSamplingCount, aznumeric_cast<uint16_t>(count));
@ -517,6 +534,7 @@ namespace AZ
SetDebugColoringEnabled(m_configuration.m_isDebugColoringEnabled);
SetShadowFilterMethod(m_configuration.m_shadowFilterMethod);
SetShadowBias(m_configuration.m_shadowBias);
SetNormalShadowBias(m_configuration.m_normalShadowBias);
SetFilteringSampleCount(m_configuration.m_filteringSampleCount);
SetShadowReceiverPlaneBiasEnabled(m_configuration.m_receiverPlaneBiasEnabled);

@ -81,7 +81,9 @@ namespace AZ
bool GetShadowReceiverPlaneBiasEnabled() const override;
void SetShadowReceiverPlaneBiasEnabled(bool enable) override;
float GetShadowBias() const override;
void SetShadowBias(float width) override;
void SetShadowBias(float bias) override;
float GetNormalShadowBias() const override;
void SetNormalShadowBias(float bias) override;
private:
friend class EditorDirectionalLightComponent;

@ -136,7 +136,7 @@ namespace AZ
->Attribute(Edit::Attributes::Min, 0.0f)
->Attribute(Edit::Attributes::Max, 100.0f)
->Attribute(Edit::Attributes::SoftMin, 0.0f)
->Attribute(Edit::Attributes::SoftMax, 1.0f)
->Attribute(Edit::Attributes::SoftMax, 2.0f)
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->Attribute(Edit::Attributes::Visibility, &AreaLightComponentConfig::SupportsShadows)
->Attribute(Edit::Attributes::ReadOnly, &AreaLightComponentConfig::ShadowsDisabled)

@ -134,7 +134,7 @@ namespace AZ
->EnumAttribute(ShadowFilterMethod::EsmPcf, "ESM+PCF")
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->DataElement(Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_filteringSampleCount, "Filtering sample count\n",
"This is used only when the pixel is predicted as on the boundary.\n"
"This is used only when the pixel is predicted to be on the boundary.\n"
"Specific to PCF and ESM+PCF.")
->Attribute(Edit::Attributes::Min, 4)
->Attribute(Edit::Attributes::Max, 64)
@ -154,6 +154,13 @@ namespace AZ
->Attribute(Edit::Attributes::Min, 0.f)
->Attribute(Edit::Attributes::Max, 0.2)
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->DataElement(
Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_normalShadowBias, "Normal Shadow Bias\n",
"Reduces acne by biasing the shadowmap lookup along the geometric normal.\n"
"If this is 0, no biasing is applied.")
->Attribute(Edit::Attributes::Min, 0.f)
->Attribute(Edit::Attributes::Max, 10.0f)
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
;
}
}

@ -100,9 +100,13 @@ namespace AZ
->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_whiteBalanceKelvin, "Temperature", "Temperature in Kelvin")
->Attribute(Edit::Attributes::Min, 1000.0f)
->Attribute(Edit::Attributes::Max, 40000.0f)
->Attribute(AZ::Edit::Attributes::SliderCurveMidpoint, 0.165f)
->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_whiteBalanceTint, "Tint", "Tint Value")
->Attribute(Edit::Attributes::Min, -100.0f)
->Attribute(Edit::Attributes::Max, 100.0f)
->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_whiteBalanceLuminancePreservation, "Luminance Preservation", "Modulate the preservation of luminance")
->Attribute(Edit::Attributes::Min, 0.0f)
->Attribute(Edit::Attributes::Max, 1.0f)
->ClassElement(AZ::Edit::ClassElements::Group, "Split Toning")
->Attribute(AZ::Edit::Attributes::AutoExpand, true)

@ -465,7 +465,7 @@ void ApplyLighting(inout Surface surface, inout LightingData lightingData)
const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight;
if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount)
{
DirectionalLightShadow::GetShadowCoords(shadowIndex, surface.position, lightingData.shadowCoords);
DirectionalLightShadow::GetShadowCoords(shadowIndex, surface.position, surface.vertexNormal, lightingData.shadowCoords);
}
// Light loops application.

@ -209,6 +209,7 @@ namespace Blast
AZStd::shared_ptr<Physics::Shape>(
const Physics::ColliderConfiguration&, const Physics::ShapeConfiguration&));
MOCK_METHOD1(ReleaseNativeMeshObject, void(void*));
MOCK_METHOD1(ReleaseNativeHeightfieldObject, void(void*));
MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr<Physics::Material>(const Physics::MaterialConfiguration&));
MOCK_METHOD0(GetDefaultMaterial, AZStd::shared_ptr<Physics::Material>());
MOCK_METHOD1(

@ -46,6 +46,7 @@ namespace Physics
}
MOCK_METHOD2(CreateShape, AZStd::shared_ptr<Physics::Shape>(const Physics::ColliderConfiguration& colliderConfiguration, const Physics::ShapeConfiguration& configuration));
MOCK_METHOD1(ReleaseNativeMeshObject, void(void* nativeMeshObject));
MOCK_METHOD1(ReleaseNativeHeightfieldObject, void(void* nativeMeshObject));
MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr<Physics::Material>(const Physics::MaterialConfiguration& materialConfiguration));
MOCK_METHOD3(CookConvexMeshToFile, bool(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount));
MOCK_METHOD3(CookConvexMeshToMemory, bool(const AZ::Vector3* vertices, AZ::u32 vertexCount, AZStd::vector<AZ::u8>& result));

@ -107,7 +107,16 @@ endif()
# Tests
################################################################################
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
ly_add_target(
NAME GradientSignal.Mocks HEADERONLY
NAMESPACE Gem
FILES_CMAKE
gradientsignal_mocks_files.cmake
INCLUDE_DIRECTORIES
INTERFACE
Mocks
)
ly_add_target(
NAME GradientSignal.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
NAMESPACE Gem
@ -122,6 +131,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
AZ::AzTest
Gem::GradientSignal.Static
Gem::LmbrCentral
Gem::GradientSignal.Mocks
)
ly_add_googletest(
NAME Gem::GradientSignal.Tests

@ -0,0 +1,34 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <gmock/gmock.h>
#include <AzCore/Component/ComponentApplication.h>
#include <GradientSignal/Ebuses/GradientRequestBus.h>
namespace UnitTest
{
class MockGradientRequests
: private GradientSignal::GradientRequestBus::Handler
{
public:
MockGradientRequests(AZ::EntityId entityId)
{
GradientSignal::GradientRequestBus::Handler::BusConnect(entityId);
}
~MockGradientRequests()
{
GradientSignal::GradientRequestBus::Handler::BusDisconnect();
}
MOCK_CONST_METHOD1(GetValue, float(const GradientSignal::GradientSampleParams&));
MOCK_CONST_METHOD1(IsEntityInHierarchy, bool(const AZ::EntityId&));
};
} // namespace UnitTest

@ -0,0 +1,11 @@
#
# Copyright (c) Contributors to the Open 3D Engine Project.
# For complete copyright and license terms please see the LICENSE at the root of this distribution.
#
# SPDX-License-Identifier: Apache-2.0 OR MIT
#
#
set(FILES
Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h
)

@ -15,12 +15,6 @@
namespace LmbrCentral
{
/// Type ID for the AxisAlignedBoxShapeComponent
static const AZ::Uuid AxisAlignedBoxShapeComponentTypeId = "{641D817E-1BC6-406A-BBB2-218541808E45}";
/// Type ID for the EditorAxisAlignedBoxShapeComponent
static const AZ::Uuid EditorAxisAlignedBoxShapeComponentTypeId = "{8C027DF6-E157-4159-9BF8-F1B925466F1F}";
/// Provide a Component interface for AxisAlignedBoxShape functionality.
class AxisAlignedBoxShapeComponent
: public AZ::Component

@ -24,6 +24,12 @@ namespace LmbrCentral
/// Type ID for the BoxShapeConfig
static const AZ::Uuid BoxShapeConfigTypeId = "{F034FBA2-AC2F-4E66-8152-14DFB90D6283}";
/// Type ID for the AxisAlignedBoxShapeComponent
static const AZ::Uuid AxisAlignedBoxShapeComponentTypeId = "{641D817E-1BC6-406A-BBB2-218541808E45}";
/// Type ID for the EditorAxisAlignedBoxShapeComponent
static const AZ::Uuid EditorAxisAlignedBoxShapeComponentTypeId = "{8C027DF6-E157-4159-9BF8-F1B925466F1F}";
/// Configuration data for BoxShapeComponent
class BoxShapeConfig
: public ShapeComponentConfig

@ -58,11 +58,10 @@ namespace Multiplayer
void BindNetworkHierarchyLeaveEventHandler(NetworkHierarchyLeaveEvent::Handler& handler) override;
//! @}
protected:
private:
//! Used by @NetworkHierarchyRootComponent
void SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot);
void SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot);
private:
AZ::ChildChangedEvent::Handler m_childChangedHandler;
void OnChildChanged(AZ::ChildChangeType type, AZ::EntityId child);
@ -80,5 +79,8 @@ namespace Multiplayer
bool m_isHierarchyEnabled = true;
void NotifyChildrenHierarchyDisbanded();
AzNetworking::ConnectionId m_previousOwningConnectionId = AzNetworking::InvalidConnectionId;
void SetOwningConnectionId(AzNetworking::ConnectionId connectionId) override;
};
}

@ -60,10 +60,9 @@ namespace Multiplayer
bool SerializeEntityCorrection(AzNetworking::ISerializer& serializer);
protected:
void SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot);
private:
void SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot);
AZ::ChildChangedEvent::Handler m_childChangedHandler;
AZ::ParentChangedEvent::Handler m_parentChangedHandler;
@ -80,16 +79,19 @@ namespace Multiplayer
//! Rebuilds hierarchy starting from this root component's entity.
void RebuildHierarchy();
//! @param underEntity Walk the child entities that belong to @underEntity and consider adding them to the hierarchy.
//! Builds the hierarchy using breadth-first iterative method.
void InternalBuildHierarchyList(AZ::Entity* underEntity);
void SetRootForEntity(AZ::Entity* root, const AZ::Entity* childEntity);
void SetRootForEntity(AZ::Entity* previousKnownRoot, AZ::Entity* newRoot, const AZ::Entity* childEntity);
//! Set to false when deactivating or otherwise not to be included in hierarchy considerations.
bool m_isHierarchyEnabled = true;
AzNetworking::ConnectionId m_previousOwningConnectionId = AzNetworking::InvalidConnectionId;
void SetOwningConnectionId(AzNetworking::ConnectionId connectionId) override;
friend class HierarchyBenchmarkBase;
};

@ -129,34 +129,48 @@ namespace Multiplayer
handler.Connect(m_networkHierarchyLeaveEvent);
}
void NetworkHierarchyChildComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot)
void NetworkHierarchyChildComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot)
{
if (m_rootEntity != hierarchyRoot)
if (newHierarchyRoot)
{
m_rootEntity = hierarchyRoot;
if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority)
if (m_rootEntity != newHierarchyRoot)
{
NetworkHierarchyChildComponentController* controller = static_cast<NetworkHierarchyChildComponentController*>(GetController());
if (m_rootEntity)
m_rootEntity = newHierarchyRoot;
if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority)
{
NetworkHierarchyChildComponentController* controller = static_cast<NetworkHierarchyChildComponentController*>(GetController());
const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId());
controller->SetHierarchyRoot(netRootId);
m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId());
}
else
{
controller->SetHierarchyRoot(InvalidNetEntityId);
m_networkHierarchyLeaveEvent.Signal();
}
GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent<NetBindComponent>()->GetOwningConnectionId());
m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId());
}
}
else if ((previousHierarchyRoot && m_rootEntity == previousHierarchyRoot) || !previousHierarchyRoot)
{
m_rootEntity = nullptr;
if (m_rootEntity == nullptr)
if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority)
{
NotifyChildrenHierarchyDisbanded();
NetworkHierarchyChildComponentController* controller = static_cast<NetworkHierarchyChildComponentController*>(GetController());
controller->SetHierarchyRoot(InvalidNetEntityId);
}
GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId);
m_networkHierarchyLeaveEvent.Signal();
NotifyChildrenHierarchyDisbanded();
}
}
void NetworkHierarchyChildComponent::SetOwningConnectionId(AzNetworking::ConnectionId connectionId)
{
NetworkHierarchyChildComponentBase::SetOwningConnectionId(connectionId);
if (IsHierarchicalChild() == false)
{
m_previousOwningConnectionId = connectionId;
}
}
@ -180,14 +194,18 @@ namespace Multiplayer
if (m_rootEntity != newRoot)
{
m_rootEntity = newRoot;
m_previousOwningConnectionId = GetNetBindComponent()->GetOwningConnectionId();
GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent<NetBindComponent>()->GetOwningConnectionId());
m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId());
}
}
else
{
GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId);
m_isHierarchyEnabled = false;
m_rootEntity = nullptr;
m_networkHierarchyLeaveEvent.Signal();
}
}
@ -203,11 +221,11 @@ namespace Multiplayer
{
if (auto* hierarchyChildComponent = childEntity->FindComponent<NetworkHierarchyChildComponent>())
{
hierarchyChildComponent->SetTopLevelHierarchyRootEntity(nullptr);
hierarchyChildComponent->SetTopLevelHierarchyRootEntity(nullptr, nullptr);
}
else if (auto* hierarchyRootComponent = childEntity->FindComponent<NetworkHierarchyRootComponent>())
{
hierarchyRootComponent->SetTopLevelHierarchyRootEntity(nullptr);
hierarchyRootComponent->SetTopLevelHierarchyRootEntity(nullptr, nullptr);
}
}
}

@ -107,7 +107,7 @@ namespace Multiplayer
{
if (const AZ::Entity* childEntity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(childEntityId))
{
SetRootForEntity(nullptr, childEntity);
SetRootForEntity(GetEntity(), nullptr, childEntity);
}
}
}
@ -209,7 +209,7 @@ namespace Multiplayer
auto [rootComponent, childComponent] = GetHierarchyComponents(parentEntity);
if (rootComponent == nullptr && childComponent == nullptr)
{
RebuildHierarchy();
SetRootForEntity(nullptr, nullptr, GetEntity());
}
else
{
@ -219,7 +219,7 @@ namespace Multiplayer
else
{
// Detached from parent
RebuildHierarchy();
SetRootForEntity(nullptr, nullptr, GetEntity());
}
}
@ -247,14 +247,14 @@ namespace Multiplayer
{
// This is a newly added entity to the network hierarchy.
hierarchyChanged = true;
SetRootForEntity(GetEntity(), currentEntity);
SetRootForEntity(nullptr, GetEntity(), currentEntity);
}
}
// These entities were removed since last rebuild.
for (const AZ::Entity* previousEntity : previousEntities)
{
SetRootForEntity(nullptr, previousEntity);
SetRootForEntity(GetEntity(), nullptr, previousEntity);
}
if (!previousEntities.empty())
@ -307,45 +307,66 @@ namespace Multiplayer
}
}
void NetworkHierarchyRootComponent::SetRootForEntity(AZ::Entity* root, const AZ::Entity* childEntity)
void NetworkHierarchyRootComponent::SetRootForEntity(AZ::Entity* previousKnownRoot, AZ::Entity* newRoot, const AZ::Entity* childEntity)
{
auto [hierarchyRootComponent, hierarchyChildComponent] = GetHierarchyComponents(childEntity);
if (hierarchyChildComponent)
{
hierarchyChildComponent->SetTopLevelHierarchyRootEntity(root);
hierarchyChildComponent->SetTopLevelHierarchyRootEntity(previousKnownRoot, newRoot);
}
else if (hierarchyRootComponent)
{
hierarchyRootComponent->SetTopLevelHierarchyRootEntity(root);
hierarchyRootComponent->SetTopLevelHierarchyRootEntity(previousKnownRoot, newRoot);
}
}
void NetworkHierarchyRootComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot)
void NetworkHierarchyRootComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot)
{
m_rootEntity = hierarchyRoot;
if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority)
if (newHierarchyRoot)
{
NetworkHierarchyChildComponentController* controller = static_cast<NetworkHierarchyChildComponentController*>(GetController());
if (hierarchyRoot)
if (m_rootEntity != newHierarchyRoot)
{
const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(hierarchyRoot->GetId());
controller->SetHierarchyRoot(netRootId);
m_rootEntity = newHierarchyRoot;
if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority)
{
NetworkHierarchyRootComponentController* controller = static_cast<NetworkHierarchyRootComponentController*>(GetController());
const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId());
controller->SetHierarchyRoot(netRootId);
}
GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent<NetBindComponent>()->GetOwningConnectionId());
m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId());
}
else
}
else if ((previousHierarchyRoot && m_rootEntity == previousHierarchyRoot) || !previousHierarchyRoot)
{
m_rootEntity = nullptr;
if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority)
{
NetworkHierarchyRootComponentController* controller = static_cast<NetworkHierarchyRootComponentController*>(GetController());
controller->SetHierarchyRoot(InvalidNetEntityId);
}
}
if (m_rootEntity == nullptr)
{
GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId);
m_networkHierarchyLeaveEvent.Signal();
// We lost the parent hierarchical entity, so as a root we need to re-build our own hierarchy.
RebuildHierarchy();
}
}
void NetworkHierarchyRootComponent::SetOwningConnectionId(AzNetworking::ConnectionId connectionId)
{
NetworkHierarchyRootComponentBase::SetOwningConnectionId(connectionId);
if (IsHierarchicalChild() == false)
{
m_previousOwningConnectionId = connectionId;
}
}
NetworkHierarchyRootComponentController::NetworkHierarchyRootComponentController(NetworkHierarchyRootComponent& parent)
: NetworkHierarchyRootComponentControllerBase(parent)
{
@ -370,7 +391,7 @@ namespace Multiplayer
void NetworkHierarchyRootComponentController::CreateInput(Multiplayer::NetworkInput& input, float deltaTime)
{
NetworkHierarchyRootComponent& component = GetParent();
if(!component.IsHierarchicalRoot())
if (!component.IsHierarchicalRoot())
{
return;
}
@ -386,7 +407,7 @@ namespace Multiplayer
for (AZ::Entity* child : entities)
{
if(child == component.GetEntity())
if (child == component.GetEntity())
{
continue; // Avoid infinite recursion
}

@ -302,6 +302,36 @@ namespace Multiplayer
SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId);
}
TEST_F(ClientSimpleHierarchyTests, ChildHasOwningConnectionIdOfParent)
{
// disconnect and assign new connection ids
SetParentIdOnNetworkTransform(m_child->m_entity, InvalidNetEntityId);
SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId);
m_root->m_entity->FindComponent<NetBindComponent>()->SetOwningConnectionId(ConnectionId{ 1 });
m_child->m_entity->FindComponent<NetBindComponent>()->SetOwningConnectionId(ConnectionId{ 2 });
const ConnectionId previousConnectionId = m_child->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId();
// re-attach, child's owning connection id should then be root's connection id
SetParentIdOnNetworkTransform(m_child->m_entity, RootNetEntityId);
SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, RootNetEntityId);
EXPECT_EQ(
m_child->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId(),
m_root->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId()
);
// detach, the child should roll back to his previous owning connection id
SetParentIdOnNetworkTransform(m_child->m_entity, InvalidNetEntityId);
SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId);
EXPECT_EQ(
m_child->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId(),
previousConnectionId
);
}
/*
* Parent -> Child -> ChildOfChild
*/
@ -396,7 +426,7 @@ namespace Multiplayer
using MultiplayerTest::TestMultiplayerComponentNetworkInput;
auto* rootNetBind = m_root->m_entity->FindComponent<NetBindComponent>();
NetworkInputArray inputArray(rootNetBind->GetEntityHandle());
NetworkInput& input = inputArray[0];

@ -195,6 +195,49 @@ namespace Multiplayer
m_child->m_entity.reset();
}
TEST_F(ServerSimpleHierarchyTests, ChildPointsToRootAfterReattachment)
{
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(AZ::EntityId());
EXPECT_EQ(
m_child->m_entity->FindComponent<NetworkHierarchyChildComponent>()->GetHierarchyRoot(),
InvalidNetEntityId
);
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(m_root->m_entity->GetId());
EXPECT_EQ(
m_child->m_entity->FindComponent<NetworkHierarchyChildComponent>()->GetHierarchyRoot(),
m_root->m_entity->FindComponent<NetBindComponent>()->GetNetEntityId()
);
}
TEST_F(ServerSimpleHierarchyTests, ChildHasOwningConnectionIdOfParent)
{
// disconnect and assign new connection ids
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(AZ::EntityId());
m_root->m_entity->FindComponent<NetBindComponent>()->SetOwningConnectionId(ConnectionId{ 1 });
m_child->m_entity->FindComponent<NetBindComponent>()->SetOwningConnectionId(ConnectionId{ 2 });
const ConnectionId previousConnectionId = m_child->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId();
// re-attach, child's owning connection id should then be root's connection id
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(m_root->m_entity->GetId());
EXPECT_EQ(
m_child->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId(),
m_root->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId()
);
// detach, the child should roll back to his previous owning connection id
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(AZ::EntityId());
EXPECT_EQ(
m_child->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId(),
previousConnectionId
);
}
/*
* Parent -> Child -> ChildOfChild
*/
@ -410,7 +453,7 @@ namespace Multiplayer
{
MockNetworkHierarchyCallbackHandler mock;
EXPECT_CALL(mock, OnNetworkHierarchyUpdated(m_root->m_entity->GetId())).Times(2);
m_root->m_entity->FindComponent<NetworkHierarchyRootComponent>()->BindNetworkHierarchyChangedEventHandler(mock.m_changedHandler);
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(AZ::EntityId());
@ -822,6 +865,22 @@ namespace Multiplayer
}
}
TEST_F(ServerHierarchyOfHierarchyTests, InnerChildrenPointToInnerRootAfterDetachmentFromTopRoot)
{
m_root2->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(m_root->m_entity->GetId());
// detach
m_root2->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(AZ::EntityId());
EXPECT_EQ(
m_child2->m_entity->FindComponent<NetworkHierarchyChildComponent>()->GetHierarchyRoot(),
m_root2->m_entity->FindComponent<NetBindComponent>()->GetNetEntityId()
);
EXPECT_EQ(
m_childOfChild2->m_entity->FindComponent<NetworkHierarchyChildComponent>()->GetHierarchyRoot(),
m_root2->m_entity->FindComponent<NetBindComponent>()->GetNetEntityId()
);
}
TEST_F(ServerHierarchyOfHierarchyTests, Inner_Root_Has_Child_References_After_Detachment_From_Child_Of_Child)
{
m_root2->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(m_childOfChild->m_entity->GetId());
@ -1005,6 +1064,59 @@ namespace Multiplayer
m_console->GetCvarValue<uint32_t>("bg_hierarchyEntityMaxLimit", currentMaxLimit);
}
TEST_F(ServerHierarchyOfHierarchyTests, InnerRootAndItsChildrenHaveOwningConnectionIdOfTopRoot)
{
// Assign new connection ids.
m_root->m_entity->FindComponent<NetBindComponent>()->SetOwningConnectionId(ConnectionId{ 1 });
m_root2->m_entity->FindComponent<NetBindComponent>()->SetOwningConnectionId(ConnectionId{ 2 });
// Attach then inner hierarchy's owning connection id should then be top root's connection id.
m_root2->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(m_childOfChild->m_entity->GetId());
EXPECT_EQ(
m_root2->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId(),
m_root->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId()
);
EXPECT_EQ(
m_child2->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId(),
m_root->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId()
);
EXPECT_EQ(
m_childOfChild2->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId(),
m_root->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId()
);
}
TEST_F(ServerHierarchyOfHierarchyTests, InnerRootAndItsChildrenHaveTheirOriginalOwningConnectionIdAfterDetachingFromTopRoot)
{
// Assign new connection ids.
m_root->m_entity->FindComponent<NetBindComponent>()->SetOwningConnectionId(ConnectionId{ 1 });
m_root2->m_entity->FindComponent<NetBindComponent>()->SetOwningConnectionId(ConnectionId{ 2 });
// Attach then inner hierarchy's owning connection id should then be top root's connection id.
m_root2->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(m_childOfChild->m_entity->GetId());
// detach, inner hierarchy should roll back to his previous owning connection id
m_root2->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(AZ::EntityId());
EXPECT_EQ(
m_root2->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId(),
ConnectionId{ 2 }
);
EXPECT_EQ(
m_child2->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId(),
m_root2->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId()
);
EXPECT_EQ(
m_childOfChild2->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId(),
m_root2->m_entity->FindComponent<NetBindComponent>()->GetOwningConnectionId()
);
}
/*
* Parent -> Child -> ChildOfChild (not marked as in a hierarchy)
*/
@ -1242,15 +1354,15 @@ namespace Multiplayer
);
}
TEST_F(ServerHierarchyWithThreeRoots, ReattachMiddleChildWhileLastChildGetsLeaveEventOnce)
TEST_F(ServerHierarchyWithThreeRoots, InnerRootLeftTopRootThenLastChildGetsJoinedEventOnce)
{
m_root2->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(m_childOfChild->m_entity->GetId());
m_root3->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(m_childOfChild->m_entity->GetId());
MockNetworkHierarchyCallbackHandler mock;
EXPECT_CALL(mock, OnNetworkHierarchyLeave());
m_childOfChild3->m_entity->FindComponent<NetworkHierarchyChildComponent>()->BindNetworkHierarchyLeaveEventHandler(mock.m_leaveHandler);
EXPECT_CALL(mock, OnNetworkHierarchyUpdated(m_root3->m_entity->GetId()));
m_childOfChild3->m_entity->FindComponent<NetworkHierarchyChildComponent>()->BindNetworkHierarchyChangedEventHandler(mock.m_changedHandler);
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(AZ::EntityId());
}

@ -676,7 +676,17 @@ namespace PhysX
}
}
AZ::Transform Collider::GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig,
void Collider::DrawHeightfield(
[[maybe_unused]] AzFramework::DebugDisplayRequests& debugDisplay,
[[maybe_unused]] const Physics::ColliderConfiguration& colliderConfig,
[[maybe_unused]] const Physics::HeightfieldShapeConfiguration& heightfieldShapeConfig,
[[maybe_unused]] const AZ::Vector3& colliderScale,
[[maybe_unused]] const bool forceUniformScaling) const
{
}
AZ::Transform Collider::GetColliderLocalTransform(
const Physics::ColliderConfiguration& colliderConfig,
const AZ::Vector3& colliderScale) const
{
// Apply entity world transform scale to collider offset

@ -104,7 +104,15 @@ namespace PhysX
const AZ::Vector3& meshScale,
AZ::u32 geomIndex) const;
void DrawPolygonPrism(AzFramework::DebugDisplayRequests& debugDisplay,
void DrawHeightfield(
AzFramework::DebugDisplayRequests& debugDisplay,
const Physics::ColliderConfiguration& colliderConfig,
const Physics::HeightfieldShapeConfiguration& heightfieldShapeConfig,
const AZ::Vector3& colliderScale = AZ::Vector3::CreateOne(),
const bool forceUniformScaling = false) const;
void DrawPolygonPrism(
AzFramework::DebugDisplayRequests& debugDisplay,
const Physics::ColliderConfiguration& colliderConfig, const AZStd::vector<AZ::Vector3>& points) const;
AZ::Transform GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig,

@ -16,19 +16,21 @@ namespace AzPhysics
{
class CollisionGroup;
class CollisionLayer;
}
} // namespace AzPhysics
namespace physx
{
class PxScene;
class PxSceneDesc;
class PxConvexMesh;
class PxHeightField;
class PxTriangleMesh;
class PxShape;
class PxCooking;
class PxControllerManager;
struct PxFilterData;
}
struct PxHeightFieldSample;
} // namespace physx
namespace PhysX
{
@ -63,6 +65,13 @@ namespace PhysX
/// @return Pointer to the created mesh.
virtual physx::PxTriangleMesh* CreateTriangleMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) = 0;
/// Creates a new heightfield.
/// @param samples Pointer to beginning of heightfield sample data.
/// @param numRows Number of rows in the heightfield.
/// @param numColumns Number of columns in the heightfield.
/// @return Pointer to the created heightfield.
virtual physx::PxHeightField* CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns) = 0;
/// Creates PhysX collision filter data from generic collision filtering settings.
/// @param layer The collision layer the object belongs to.
/// @param group The set of collision layers the object will interact with.

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

Loading…
Cancel
Save