Merge branch 'development' into Prism/ShowRepoGems

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

@ -9,10 +9,13 @@
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. 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. 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_PROJECT_NAME=AWSAUTO
Set O3DE_AWS_DEPLOY_REGION=us-east-1 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 ASSUME_ROLE_ARN=arn:aws:iam::{your_aws_account_id}:role/o3de-automation-tests
Set COMMIT_ID=HEAD Set COMMIT_ID=HEAD
```
4. In the same Command Prompt window, Deploy the CDK applications for AWS gems by running deploy_cdk_applications.cmd. 4. In the same Command Prompt window, Deploy the CDK applications for AWS gems by running deploy_cdk_applications.cmd.
## Run Automation Tests ## Run Automation Tests

@ -17,3 +17,392 @@ LIGHT_TYPES = {
'simple_point': 6, 'simple_point': 6,
'simple_spot': 7, '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.asset_utils import Asset
from editor_python_test_tools.editor_entity_utils import EditorEntity 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: with Tracer() as error_tracer:
# Test setup begins. # Test setup begins.
# Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level. # Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level.
helper.init_idle() TestHelper.init_idle()
helper.open_level("", "Base") TestHelper.open_level("", "Base")
# Test steps begin. # Test steps begin.
# 1. Create a Mesh entity with no components. # 1. Create a Mesh entity with no components.
mesh_name = "Mesh" mesh_entity = EditorEntity.create_editor_entity(Atom.mesh())
mesh_entity = EditorEntity.create_editor_entity(mesh_name)
Report.critical_result(Tests.mesh_entity_creation, mesh_entity.exists()) Report.critical_result(Tests.mesh_entity_creation, mesh_entity.exists())
# 2. Add a Mesh component to Mesh entity. # 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( Report.critical_result(
Tests.mesh_component_added, Tests.mesh_component_added,
mesh_entity.has_component(mesh_name)) mesh_entity.has_component(Atom.mesh()))
# 3. UNDO the entity creation and component addition. # 3. UNDO the entity creation and component addition.
# -> UNDO component addition. # -> UNDO component addition.
@ -125,17 +125,16 @@ def AtomEditorComponents_Mesh_AddedToEntity():
Report.result(Tests.creation_redo, mesh_entity.exists()) Report.result(Tests.creation_redo, mesh_entity.exists())
# 5. Set Mesh component asset property # 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_path = os.path.join('Objects', 'shaderball', 'shaderball_default_1m.azmodel')
model = Asset.find_asset_by_path(model_path) 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, 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. # 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) general.idle_wait_frames(1)
helper.exit_game_mode(Tests.exit_game_mode) TestHelper.exit_game_mode(Tests.exit_game_mode)
# 7. Test IsHidden. # 7. Test IsHidden.
mesh_entity.set_visibility_state(False) mesh_entity.set_visibility_state(False)
@ -159,7 +158,7 @@ def AtomEditorComponents_Mesh_AddedToEntity():
Report.result(Tests.deletion_redo, not mesh_entity.exists()) Report.result(Tests.deletion_redo, not mesh_entity.exists())
# 12. Look for errors or asserts. # 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: for error_info in error_tracer.errors:
Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}") Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}")
for assert_info in error_tracer.asserts: for assert_info in error_tracer.asserts:

@ -16,18 +16,11 @@
#include <QScopedValueRollback> #include <QScopedValueRollback>
#include <QToolBar> #include <QToolBar>
#include <QLoggingCategory> #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/Component/ComponentApplication.h>
#include <AzCore/IO/Path/Path.h> #include <AzCore/IO/Path/Path.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.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 // AzQtComponents
#include <AzQtComponents/Components/GlobalEventFilter.h> #include <AzQtComponents/Components/GlobalEventFilter.h>
@ -39,7 +32,6 @@
#include "Settings.h" #include "Settings.h"
#include "CryEdit.h" #include "CryEdit.h"
enum enum
{ {
// in milliseconds // in milliseconds
@ -241,7 +233,6 @@ namespace Editor
EditorQtApplication::EditorQtApplication(int& argc, char** argv) EditorQtApplication::EditorQtApplication(int& argc, char** argv)
: AzQtApplication(argc, argv) : AzQtApplication(argc, argv)
, m_inWinEventFilter(false)
, m_stylesheet(new AzQtComponents::O3DEStylesheet(this)) , m_stylesheet(new AzQtComponents::O3DEStylesheet(this))
, m_idleTimer(new QTimer(this)) , m_idleTimer(new QTimer(this))
{ {
@ -368,86 +359,10 @@ namespace Editor
UninstallEditorTranslators(); UninstallEditorTranslators();
} }
#if defined(AZ_PLATFORM_WINDOWS) EditorQtApplication* EditorQtApplication::instance()
bool EditorQtApplication::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 static_cast<EditorQtApplication*>(QApplication::instance());
}
return true;
}
}
return false;
} }
#endif
void EditorQtApplication::OnEditorNotifyEvent(EEditorNotifyEvent event) void EditorQtApplication::OnEditorNotifyEvent(EEditorNotifyEvent event)
{ {
@ -505,11 +420,6 @@ namespace Editor
return m_stylesheet->GetColorByName(name); return m_stylesheet->GetColorByName(name);
} }
EditorQtApplication* EditorQtApplication::instance()
{
return static_cast<EditorQtApplication*>(QApplication::instance());
}
bool EditorQtApplication::IsActive() bool EditorQtApplication::IsActive()
{ {
return applicationState() == Qt::ApplicationActive; return applicationState() == Qt::ApplicationActive;
@ -613,42 +523,6 @@ namespace Editor
case QEvent::KeyRelease: case QEvent::KeyRelease:
m_pressedKeys.remove(reinterpret_cast<QKeyEvent*>(event)->key()); m_pressedKeys.remove(reinterpret_cast<QKeyEvent*>(event)->key());
break; 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: default:
break; break;
} }

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

@ -4135,9 +4135,9 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[])
Editor::EditorQtApplication::InstallQtLogHandler(); Editor::EditorQtApplication::InstallQtLogHandler();
AzQtComponents::Utilities::HandleDpiAwareness(AzQtComponents::Utilities::SystemDpiAware); 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 // 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. // 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; return -1;
} }
AzToolsFramework::EditorEvents::Bus::Broadcast(&AzToolsFramework::EditorEvents::NotifyQtApplicationAvailable, &app); 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
int exitCode = 0; int exitCode = 0;
@ -4189,9 +4184,9 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[])
if (didCryEditStart) if (didCryEditStart)
{ {
app.EnableOnIdle(); app->EnableOnIdle();
ret = app.exec(); ret = app->exec();
} }
else else
{ {
@ -4202,6 +4197,8 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[])
} }
delete app;
gSettings.Disconnect(); gSettings.Disconnect();
return ret; return ret;

@ -6,7 +6,7 @@
* *
*/ */
#include "QtEditorApplication.h" #include "QtEditorApplication_linux.h"
#ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB #ifdef PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB
#include <AzFramework/XcbEventHandler.h> #include <AzFramework/XcbEventHandler.h>
@ -14,7 +14,16 @@
namespace Editor 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()) 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 set(FILES
../../Core/QtEditorApplication_linux.cpp Editor/Core/QtEditorApplication_linux.cpp
../Common/Unimplemented/Util/Mailer_Unimplemented.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 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; NSEvent* event = (NSEvent*)message;
if (GetIEditor()->IsInGameMode()) if (GetIEditor()->IsInGameMode())

@ -7,7 +7,7 @@
# #
set(FILES set(FILES
../../Core/QtEditorApplication_mac.mm Editor/Core/QtEditorApplication_mac.mm
../../LogFile_mac.mm ../../LogFile_mac.mm
../../WindowObserver_mac.h ../../WindowObserver_mac.h
../../WindowObserver_mac.mm ../../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 set(FILES
Editor/Core/QtEditorApplication_windows.cpp
Util/Mailer_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(Sphere);
REFLECT_SHAPETYPE_ENUM_VALUE(Cylinder); REFLECT_SHAPETYPE_ENUM_VALUE(Cylinder);
REFLECT_SHAPETYPE_ENUM_VALUE(PhysicsAsset); REFLECT_SHAPETYPE_ENUM_VALUE(PhysicsAsset);
REFLECT_SHAPETYPE_ENUM_VALUE(Heightfield);
#undef REFLECT_SHAPETYPE_ENUM_VALUE #undef REFLECT_SHAPETYPE_ENUM_VALUE
} }
@ -305,4 +306,125 @@ namespace Physics
m_cachedNativeMesh = nullptr; 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 #pragma once
#include <AzCore/Math/Vector3.h> #include <AzCore/Math/Vector3.h>
#include <AzCore/Math/Vector2.h>
#include <AzCore/Math/Quaternion.h> #include <AzCore/Math/Quaternion.h>
#include <AzCore/Asset/AssetCommon.h> #include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Serialization/SerializeContext.h> #include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Physics/HeightfieldProviderBus.h>
namespace Physics namespace Physics
{ {
/// Used to identify shape configuration type from base class. /// 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. Native, ///< Native shape configuration if user wishes to bypass generic shape configurations.
PhysicsAsset, ///< Shapes configured in the asset. PhysicsAsset, ///< Shapes configured in the asset.
CookedMesh, ///< Stores a blob of mesh data cooked for the specific engine. CookedMesh, ///< Stores a blob of mesh data cooked for the specific engine.
Heightfield ///< Interacts with the physics system heightfield
}; };
class ShapeConfiguration class ShapeConfiguration
@ -196,4 +200,52 @@ namespace Physics
mutable void* m_cachedNativeMesh = nullptr; 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 } // namespace Physics

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

@ -107,6 +107,7 @@ namespace Physics
PhysicsAssetShapeConfiguration::Reflect(context); PhysicsAssetShapeConfiguration::Reflect(context);
NativeShapeConfiguration::Reflect(context); NativeShapeConfiguration::Reflect(context);
CookedMeshShapeConfiguration::Reflect(context); CookedMeshShapeConfiguration::Reflect(context);
HeightfieldShapeConfiguration::Reflect(context);
AzPhysics::SystemInterface::Reflect(context); AzPhysics::SystemInterface::Reflect(context);
AzPhysics::Scene::Reflect(context); AzPhysics::Scene::Reflect(context);
AzPhysics::CollisionLayer::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/SimulatedBodyConfiguration.cpp
Physics/Configuration/SystemConfiguration.h Physics/Configuration/SystemConfiguration.h
Physics/Configuration/SystemConfiguration.cpp Physics/Configuration/SystemConfiguration.cpp
Physics/HeightfieldProviderBus.h
Physics/SimulatedBodies/RigidBody.h Physics/SimulatedBodies/RigidBody.h
Physics/SimulatedBodies/RigidBody.cpp Physics/SimulatedBodies/RigidBody.cpp
Physics/SimulatedBodies/StaticRigidBody.h Physics/SimulatedBodies/StaticRigidBody.h
@ -251,6 +252,7 @@ set(FILES
Physics/Shape.h Physics/Shape.h
Physics/ShapeConfiguration.h Physics/ShapeConfiguration.h
Physics/ShapeConfiguration.cpp Physics/ShapeConfiguration.cpp
Physics/HeightfieldProviderBus.h
Physics/SystemBus.h Physics/SystemBus.h
Physics/ColliderComponentBus.h Physics/ColliderComponentBus.h
Physics/RagdollPhysicsBus.h Physics/RagdollPhysicsBus.h

@ -42,6 +42,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
NAMESPACE AZ NAMESPACE AZ
FILES_CMAKE FILES_CMAKE
Tests/framework_shared_tests_files.cmake Tests/framework_shared_tests_files.cmake
AzFramework/Physics/physics_mock_files.cmake
INCLUDE_DIRECTORIES INCLUDE_DIRECTORIES
PUBLIC PUBLIC
Tests Tests

@ -13,6 +13,7 @@
#include <QIcon> #include <QIcon>
#include <QToolButton> #include <QToolButton>
#include <QPropertyAnimation> #include <QPropertyAnimation>
#include <QPainter>
namespace AzQtComponents namespace AzQtComponents
{ {
@ -27,6 +28,13 @@ namespace AzQtComponents
setAttribute(Qt::WA_ShowWithoutActivating); setAttribute(Qt::WA_ShowWithoutActivating);
setAttribute(Qt::WA_DeleteOnClose); 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); m_ui->setupUi(this);
QIcon toastIcon; QIcon toastIcon;
@ -53,6 +61,13 @@ namespace AzQtComponents
m_ui->titleLabel->setText(toastConfiguration.m_title); m_ui->titleLabel->setText(toastConfiguration.m_title);
m_ui->mainLabel->setText(toastConfiguration.m_description); 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_lifeSpan.setInterval(aznumeric_cast<int>(toastConfiguration.m_duration.count()));
m_closeOnClick = toastConfiguration.m_closeOnClick; 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() void ToastNotification::ShowToastAtCursor()
{ {
QPoint globalCursorPos = QCursor::pos(); QPoint globalCursorPos = QCursor::pos();

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

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

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

@ -80,14 +80,7 @@ namespace AzToolsFramework
// set up signals before we start thread. // set up signals before we start thread.
m_shutdownThreadSignal = false; 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(); SourceControlConnectionRequestBus::Handler::BusConnect();
SourceControlCommandBus::Handler::BusConnect(); SourceControlCommandBus::Handler::BusConnect();
@ -98,13 +91,10 @@ namespace AzToolsFramework
SourceControlCommandBus::Handler::BusDisconnect(); SourceControlCommandBus::Handler::BusDisconnect();
SourceControlConnectionRequestBus::Handler::BusDisconnect(); SourceControlConnectionRequestBus::Handler::BusDisconnect();
if (m_p4ApplicationDetected)
{
m_shutdownThreadSignal = true; // tell the thread to die. m_shutdownThreadSignal = true; // tell the thread to die.
m_WorkerSemaphore.release(1); // wake up the thread so that it sees the signal 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.join(); // wait for the thread to finish.
m_WorkerThread = AZStd::thread(); m_WorkerThread = AZStd::thread();
}
SetConnection(nullptr); SetConnection(nullptr);
} }

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

@ -177,4 +177,14 @@ namespace AzToolsFramework
DisplayQueuedNotification(); 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 OnShow();
void UpdateToastPosition(); void UpdateToastPosition();
void SetOffset(const QPoint& offset);
void SetAnchorPoint(const QPointF& anchorPoint);
private: private:
ToastId CreateToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration); ToastId CreateToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration);
void DisplayQueuedNotification(); void DisplayQueuedNotification();

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

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

@ -61,6 +61,24 @@ QTabBar::tab:focus {
color: #4082eb; 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) **************/ /************** General (Forms) **************/
#formLineEditWidget, #formLineEditWidget,
@ -505,6 +523,14 @@ QProgressBar::chunk {
background-color: #444444; background-color: #444444;
} }
#gemCatalogMenuButton {
qproperty-flat: true;
max-width:36px;
min-width:36px;
max-height:24px;
min-height:24px;
}
#GemCatalogHeaderLabel { #GemCatalogHeaderLabel {
font-size: 12px; font-size: 12px;
color: #FFFFFF; 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 <GemCatalog/GemCatalogHeaderWidget.h>
#include <AzCore/std/functional.h> #include <AzCore/std/functional.h>
#include <TagWidget.h>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMouseEvent> #include <QMouseEvent>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include <QMenu>
#include <QProgressBar> #include <QProgressBar>
#include <TagWidget.h>
#include <QMenu>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
@ -406,7 +405,6 @@ namespace O3DE::ProjectManager
CartButton* cartButton = new CartButton(gemModel, downloadController); CartButton* cartButton = new CartButton(gemModel, downloadController);
hLayout->addWidget(cartButton); hLayout->addWidget(cartButton);
hLayout->addSpacing(16); hLayout->addSpacing(16);
// Separating line // Separating line
@ -418,9 +416,9 @@ namespace O3DE::ProjectManager
hLayout->addSpacing(16); hLayout->addSpacing(16);
QMenu* gemMenu = new QMenu(this); QMenu* gemMenu = new QMenu(this);
m_openGemReposAction = gemMenu->addAction(tr("Show Gem Repos")); gemMenu->addAction( tr("Show Gem Repos"), [this]() { emit OpenGemsRepo(); });
gemMenu->addSeparator();
connect(m_openGemReposAction, &QAction::triggered, this,[this](){ emit OpenGemsRepo(); }); gemMenu->addAction( tr("Add Existing Gem"), [this]() { emit AddGem(); });
QPushButton* gemMenuButton = new QPushButton(this); QPushButton* gemMenuButton = new QPushButton(this);
gemMenuButton->setObjectName("gemCatalogMenuButton"); gemMenuButton->setObjectName("gemCatalogMenuButton");

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

@ -19,6 +19,10 @@
#include <QTimer> #include <QTimer>
#include <PythonBindingsInterface.h> #include <PythonBindingsInterface.h>
#include <QMessageBox> #include <QMessageBox>
#include <QDir>
#include <QStandardPaths>
#include <QFileDialog>
#include <QMessageBox>
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
{ {
@ -66,11 +70,15 @@ namespace O3DE::ProjectManager
hLayout->addWidget(filterWidget); hLayout->addWidget(filterWidget);
hLayout->addLayout(middleVLayout); hLayout->addLayout(middleVLayout);
hLayout->addWidget(m_gemInspector); 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) void GemCatalogScreen::ReinitForProject(const QString& projectPath)
{ {
m_gemModel->clear(); m_gemModel->clear();
m_gemsToRegisterWithProject.clear();
FillModel(projectPath); FillModel(projectPath);
if (m_filterWidget) if (m_filterWidget)
@ -86,6 +94,48 @@ namespace O3DE::ProjectManager
m_headerWidget->ReinitForProject(); m_headerWidget->ReinitForProject();
connect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetGemStatusFilter); 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 // Select the first entry after everything got correctly sized
QTimer::singleShot(200, [=]{ 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) void GemCatalogScreen::FillModel(const QString& projectPath)
{ {
AZ::Outcome<QVector<GemInfo>, AZStd::string> allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(projectPath); AZ::Outcome<QVector<GemInfo>, AZStd::string> allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(projectPath);
@ -121,6 +237,7 @@ namespace O3DE::ProjectManager
} }
m_gemModel->UpdateGemDependencies(); m_gemModel->UpdateGemDependencies();
m_notificationsEnabled = false;
// Gather enabled gems for the given project. // Gather enabled gems for the given project.
auto enabledGemNamesResult = PythonBindingsInterface::Get()->GetEnabledGemNames(projectPath); 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())); 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 else
{ {
@ -192,6 +311,12 @@ namespace O3DE::ProjectManager
return EnableDisableGemsResult::Failed; 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) for (const QModelIndex& modelIndex : toBeRemoved)

@ -10,12 +10,16 @@
#if !defined(Q_MOC_RUN) #if !defined(Q_MOC_RUN)
#include <ScreenWidget.h> #include <ScreenWidget.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzToolsFramework/UI/Notifications/ToastNotificationsView.h>
#include <GemCatalog/GemCatalogHeaderWidget.h> #include <GemCatalog/GemCatalogHeaderWidget.h>
#include <GemCatalog/GemFilterWidget.h> #include <GemCatalog/GemFilterWidget.h>
#include <GemCatalog/GemListView.h> #include <GemCatalog/GemListView.h>
#include <GemCatalog/GemInspector.h> #include <GemCatalog/GemInspector.h>
#include <GemCatalog/GemModel.h> #include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h> #include <GemCatalog/GemSortFilterProxyModel.h>
#include <QSet>
#include <QString>
#endif #endif
namespace O3DE::ProjectManager namespace O3DE::ProjectManager
@ -41,13 +45,24 @@ namespace O3DE::ProjectManager
GemModel* GetGemModel() const { return m_gemModel; } GemModel* GetGemModel() const { return m_gemModel; }
DownloadController* GetDownloadController() const { return m_downloadController; } 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: private slots:
void HandleOpenGemRepo(); void HandleOpenGemRepo();
private:
private:
void FillModel(const QString& projectPath); void FillModel(const QString& projectPath);
AZStd::unique_ptr<AzToolsFramework::ToastNotificationsView> m_notificationsView;
GemListView* m_gemListView = nullptr; GemListView* m_gemListView = nullptr;
GemInspector* m_gemInspector = nullptr; GemInspector* m_gemInspector = nullptr;
GemModel* m_gemModel = nullptr; GemModel* m_gemModel = nullptr;
@ -56,5 +71,7 @@ namespace O3DE::ProjectManager
QVBoxLayout* m_filterWidgetLayout = nullptr; QVBoxLayout* m_filterWidgetLayout = nullptr;
GemFilterWidget* m_filterWidget = nullptr; GemFilterWidget* m_filterWidget = nullptr;
DownloadController* m_downloadController = nullptr; DownloadController* m_downloadController = nullptr;
bool m_notificationsEnabled = true;
QSet<QString> m_gemsToRegisterWithProject;
}; };
} // namespace O3DE::ProjectManager } // namespace O3DE::ProjectManager

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

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

@ -557,6 +557,47 @@ namespace O3DE::ProjectManager
return AZ::Success(AZStd::move(gemNames)); 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 PythonBindings::AddProject(const QString& path)
{ {
bool registrationResult = false; bool registrationResult = false;

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

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

@ -112,13 +112,15 @@ VSOutput EnhancedPbr_ForwardPassVS(VSInput IN)
PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depth) PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depth)
{ {
const float3 vertexNormal = normalize(IN.m_normal);
// ------- Tangents & Bitangets ------- // ------- Tangents & Bitangets -------
float3 tangents[UvSetCount] = { IN.m_tangent.xyz, IN.m_tangent.xyz }; float3 tangents[UvSetCount] = { IN.m_tangent.xyz, IN.m_tangent.xyz };
float3 bitangents[UvSetCount] = { IN.m_bitangent.xyz, IN.m_bitangent.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) 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 ------- // ------- 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 uvMatrix = MaterialSrg::m_parallaxUvIndex == 0 ? MaterialSrg::m_uvMatrix : CreateIdentity3x3();
float3x3 uvMatrixInverse = MaterialSrg::m_parallaxUvIndex == 0 ? MaterialSrg::m_uvMatrixInverse : 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, ObjectSrg::GetWorldMatrix(), uvMatrix, uvMatrixInverse,
IN.m_uv[MaterialSrg::m_parallaxUvIndex], IN.m_worldPosition, depth, IN.m_position.w, displacementIsClipped); 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; const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight;
if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount) 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]; 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. 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; float detailLayerNormalFactor = MaterialSrg::m_detail_normal_factor * detailLayerBlendFactor;
surface.vertexNormal = normalize(IN.m_normal); surface.vertexNormal = vertexNormal;
surface.normal = GetDetailedNormalInputWS( surface.normal = GetDetailedNormalInputWS(
isFrontFace, IN.m_normal, 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, 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) PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depthNDC)
{ {
const float3 vertexNormal = normalize(IN.m_normal);
depthNDC = IN.m_position.z; depthNDC = IN.m_position.z;
s_blendMaskFromVertexStream = IN.m_blendMask; s_blendMaskFromVertexStream = IN.m_blendMask;
@ -321,7 +322,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
|| o_layer3_o_clearCoat_normal_useTexture || 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 ------- // ------- Debug Modes -------
@ -368,7 +369,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float
const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight; const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight;
if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount) 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); 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. // [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])); surface.normal = normalize(TangentSpaceToWorld(normalTS, IN.m_normal, tangents[MaterialSrg::m_parallaxUvIndex], bitangents[MaterialSrg::m_parallaxUvIndex]));
// ------- Combine Albedo, roughness, specular, roughness --------- // ------- Combine Albedo, roughness, specular, roughness ---------

@ -98,6 +98,8 @@ VSOutput StandardPbr_ForwardPassVS(VSInput IN)
PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depthNDC) PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depthNDC)
{ {
const float3 vertexNormal = normalize(IN.m_normal);
// ------- Tangents & Bitangets ------- // ------- Tangents & Bitangets -------
float3 tangents[UvSetCount] = { IN.m_tangent.xyz, IN.m_tangent.xyz }; float3 tangents[UvSetCount] = { IN.m_tangent.xyz, IN.m_tangent.xyz };
float3 bitangents[UvSetCount] = { IN.m_bitangent.xyz, IN.m_bitangent.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; const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight;
if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount) 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]; 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. 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, 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); 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 // licensed and released under Creative Commons 3.0 Attribution
// https://creativecommons.org/licenses/by/3.0/ // 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 // Color temperature
float3 KelvinToRgb(float kelvin) float3 KelvinToRgb(float kelvin)
{ {

@ -66,12 +66,21 @@ float3 ColorGradeSaturation (float3 frameColor, float control)
return (frameColor - vLuminance) * control + vLuminance; 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 float3 kColor = TransformColor(KelvinToRgb(kelvin), ColorSpaceId::LinearSRGB, ColorSpaceId::ACEScg);
const float luminance = CalculateLuminance(frameColor, 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. // 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) float3 ColorGrade(float3 frameColor)
{ {
frameColor = lerp(frameColor, ColorGradePostExposure(frameColor, PassSrg::m_colorGradingExposure), PassSrg::m_colorAdjustmentWeight); 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, ColorGradingContrast(frameColor, AcesCcMidGrey, PassSrg::m_colorGradingContrast), PassSrg::m_colorAdjustmentWeight);
frameColor = lerp(frameColor, ColorGradeColorFilter(frameColor, PassSrg::m_colorFilterSwatch.rgb, frameColor = lerp(frameColor, ColorGradeColorFilter(frameColor, PassSrg::m_colorFilterSwatch.rgb,
PassSrg::m_colorFilterMultiply, PassSrg::m_colorFilterIntensity), PassSrg::m_colorAdjustmentWeight); PassSrg::m_colorFilterMultiply, PassSrg::m_colorFilterIntensity), PassSrg::m_colorAdjustmentWeight);

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

@ -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( DirectionalLightShadow::GetShadowCoords(
shadowIndex, shadowIndex,
worldPosition, worldPosition,
OUT.m_normal,
OUT.m_shadowCoords); OUT.m_shadowCoords);
} }
} }

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

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

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

@ -160,6 +160,9 @@ namespace AZ
//! Reduces acne by applying a small amount of bias along shadow-space z. //! Reduces acne by applying a small amount of bias along shadow-space z.
virtual void SetShadowBias(LightHandle handle, float bias) = 0; 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 Render
} // namespace AZ } // 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(WhiteBalanceWeight, m_whiteBalanceWeight, 0.0)
AZ_GFX_FLOAT_PARAM(WhiteBalanceKelvin, m_whiteBalanceKelvin, 6600.0) AZ_GFX_FLOAT_PARAM(WhiteBalanceKelvin, m_whiteBalanceKelvin, 6600.0)
AZ_GFX_FLOAT_PARAM(WhiteBalanceTint, m_whiteBalanceTint, 0.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(SplitToneWeight, m_splitToneWeight, 0.0)
AZ_GFX_FLOAT_PARAM(SplitToneBalance, m_splitToneBalance, 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; virtual void SetAspectRatio(ShadowId id, float aspectRatio) = 0;
//! Sets the field of view for the shadow in radians in the Y direction. //! Sets the field of view for the shadow in radians in the Y direction.
virtual void SetFieldOfViewY(ShadowId id, float fieldOfView) = 0; 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; virtual void SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size) = 0;
//! Sets the shadow bias //! Sets the shadow bias.
virtual void SetShadowBias(ShadowId id, float bias) = 0; 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; virtual void SetShadowFilterMethod(ShadowId id, ShadowFilterMethod method) = 0;
//! Sets the sample count for filtering of the shadow boundary, max 64. //! Sets the sample count for filtering of the shadow boundary, max 64.
virtual void SetFilteringSampleCount(ShadowId id, uint16_t count) = 0; virtual void SetFilteringSampleCount(ShadowId id, uint16_t count) = 0;

@ -598,6 +598,15 @@ namespace AZ
m_shadowBufferNeedsUpdate = true; 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) void DirectionalLightFeatureProcessor::OnRenderPipelineAdded(RPI::RenderPipelinePtr pipeline)
{ {
PrepareForChangingRenderPipelineAndCameraView(); PrepareForChangingRenderPipelineAndCameraView();

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

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

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

@ -151,6 +151,14 @@ namespace AZ::Render
shadowProperty.m_bias = bias; 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) void ProjectedShadowFeatureProcessor::SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size)
{ {
AZ_Assert(id.IsValid(), "Invalid ShadowId passed to ProjectedShadowFeatureProcessor::SetShadowmapMaxResolution()."); 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 SetFieldOfViewY(ShadowId id, float fieldOfViewYRadians) override;
void SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size) override; void SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size) override;
void SetShadowBias(ShadowId id, float bias) override; void SetShadowBias(ShadowId id, float bias) override;
void SetNormalShadowBias(ShadowId id, float normalShadowBias) override;
void SetShadowFilterMethod(ShadowId id, ShadowFilterMethod method) override; void SetShadowFilterMethod(ShadowId id, ShadowFilterMethod method) override;
void SetFilteringSampleCount(ShadowId id, uint16_t count) override; void SetFilteringSampleCount(ShadowId id, uint16_t count) override;
void SetShadowProperties(ShadowId id, const ProjectedShadowDescriptor& descriptor) 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_shadowmapArraySlice = 0; // array slice who has shadowmap in the atlas.
uint32_t m_shadowFilterMethod = 0; // filtering method of shadows. uint32_t m_shadowFilterMethod = 0; // filtering method of shadows.
float m_boundaryScale = 0.f; // the half of boundary of lit/shadowed areas. (in degrees) 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; uint32_t m_filteringSampleCount = 0;
AZStd::array<float, 2> m_unprojectConstants = { {0, 0} }; AZStd::array<float, 2> m_unprojectConstants = { {0, 0} };
float m_bias; float m_bias;
float m_normalShadowBias;
float m_esmExponent = 87.0f; float m_esmExponent = 87.0f;
float m_padding[3]; float m_padding[3];
}; };
@ -78,6 +79,7 @@ namespace AZ::Render
ProjectedShadowDescriptor m_desc; ProjectedShadowDescriptor m_desc;
RPI::ViewPtr m_shadowmapView; RPI::ViewPtr m_shadowmapView;
float m_bias = 0.1f; float m_bias = 0.1f;
float m_normalShadowBias = 0.0f;
ShadowId m_shadowId; ShadowId m_shadowId;
}; };

@ -14,92 +14,86 @@ namespace AZ
{ {
namespace RHI namespace RHI
{ {
/** //! The platform-independent swap chain base class. Swap chains contain a "chain" of images which
* 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
* 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
* to adjust the swap chain outside of the current FrameScheduler frame. Doing so within a frame scheduler //! frame results in undefined behavior.
* frame results in undefined behavior. //!
* //! The frame scheduler controls presentation of the swap chain. The user may attach a swap chain to a scope
* 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.
* in order to render to the current image.
*/
class SwapChain class SwapChain
: public ImagePoolBase : public ImagePoolBase
{ {
public: public:
AZ_RTTI(SwapChain, "{888B64A5-D956-406F-9C33-CF6A54FC41B0}", Object);
virtual ~SwapChain(); 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); 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(); void Present();
/** //! Sets the vertical sync interval for the swap chain.
* Sets the vertical sync interval for the swap chain. //! 0 - No vsync.
* 0 - No vsync. //! N - Sync to every N vertical refresh.
* N - Sync to every N vertical refresh. //!
* //! A value of 1 syncs to the refresh rate of the monitor.
* A value of 1 syncs to the refresh rate of the monitor.
*/
void SetVerticalSyncInterval(uint32_t verticalSyncInterval); void SetVerticalSyncInterval(uint32_t verticalSyncInterval);
/** //! Resizes the display resolution of the swap chain. Ideally, this matches the platform window
* 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
* 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.
* change. Takes effect immediately and results in a GPU pipeline flush.
*/
ResultCode Resize(const SwapChainDimensions& dimensions); 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; 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; uint32_t GetCurrentImageIndex() const;
/// Returns the current image of the swap chain. //! Returns the current image of the swap chain.
Image* GetCurrentImage() const; Image* GetCurrentImage() const;
/// Returns the image associated with the provided index, where the total number of images //! Returns the image associated with the provided index, where the total number of images
/// is given by GetImageCount(). //! is given by GetImageCount().
Image* GetImage(uint32_t index) const; 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; 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; 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; } 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; } 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; } virtual bool SetExclusiveFullScreenState([[maybe_unused]]bool fullScreenState) { return false; }
AZ_RTTI(SwapChain, "{888B64A5-D956-406F-9C33-CF6A54FC41B0}", Object);
protected: protected:
SwapChain(); SwapChain();
struct InitImageRequest struct InitImageRequest
{ {
/// Pointer to the image to initialize. //! Pointer to the image to initialize.
Image* m_image = nullptr; 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; uint32_t m_imageIndex = 0;
/// Descriptor for the image. //! Descriptor for the image.
ImageDescriptor m_descriptor; ImageDescriptor m_descriptor;
}; };
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
// ResourcePool Overrides // ResourcePool Overrides
/// Called when the pool is shutting down. //! Called when the pool is shutting down.
void ShutdownInternal() override; void ShutdownInternal() override;
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
@ -111,32 +105,29 @@ namespace AZ
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
// Platform API // 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; 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; 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; virtual ResultCode ResizeInternal(const SwapChainDimensions& dimensions, SwapChainDimensions* nativeDimensions) = 0;
/// Called when the swap chain is presenting the currently swap image. //! Called when the swap chain is presenting the currently swap image.
/// Returns the index of the current image after the swap. //! Returns the index of the current image after the swap.
virtual uint32_t PresentInternal() = 0; virtual uint32_t PresentInternal() = 0;
virtual void SetVerticalSyncIntervalInternal(uint32_t previousVerticalSyncInterval) virtual void SetVerticalSyncIntervalInternal([[maybe_unused]]uint32_t previousVerticalSyncInterval) {}
{
AZ_UNUSED(previousVerticalSyncInterval);
}
////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////
SwapChainDescriptor m_descriptor; 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; AZStd::vector<Ptr<Image>> m_images;
/// The current image index. //! The current image index.
uint32_t m_currentImageIndex = 0; uint32_t m_currentImageIndex = 0;
}; };
} }

@ -55,7 +55,7 @@ namespace AZ
if (resultCode == ResultCode::Success) if (resultCode == ResultCode::Success)
{ {
m_descriptor = descriptor; 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_descriptor.m_dimensions = nativeDimensions;
m_images.reserve(m_descriptor.m_dimensions.m_imageCount); m_images.reserve(m_descriptor.m_dimensions.m_imageCount);

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

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

@ -177,8 +177,8 @@ namespace AZ
auto findIter = m_groupHandlers.find(group.GetGroupId()); auto findIter = m_groupHandlers.find(group.GetGroupId());
AZ_Assert(findIter != m_groupHandlers.end(), "Could not find group handler for groupId %d", group.GetGroupId().GetIndex()); AZ_Assert(findIter != m_groupHandlers.end(), "Could not find group handler for groupId %d", group.GetGroupId().GetIndex());
FrameGraphExecuteGroupHandlerBase* handler = findIter->second.get(); FrameGraphExecuteGroupHandlerBase* handler = findIter->second.get();
// Wait until all execute groups of the handler has finished. // 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->IsComplete()) if (!handler->IsExecuted() && handler->IsComplete())
{ {
// This will execute the recorded work into the queue. // This will execute the recorded work into the queue.
handler->End(); handler->End();

@ -61,13 +61,12 @@ namespace AZ
void SwapChain::SetVerticalSyncIntervalInternal(uint32_t previousVsyncInterval) void SwapChain::SetVerticalSyncIntervalInternal(uint32_t previousVsyncInterval)
{ {
uint32_t verticalSyncInterval = GetDescriptor().m_verticalSyncInterval; if (GetDescriptor().m_verticalSyncInterval == 0 || previousVsyncInterval == 0)
if (verticalSyncInterval == 0 || previousVsyncInterval == 0)
{ {
// The presentation mode may change when transitioning to or from a vsynced presentation mode // The presentation mode may change when transitioning to or from a vsynced presentation mode
// In this case, the swapchain must be recreated. // In this case, the swapchain must be recreated.
InvalidateNativeSwapChain(); InvalidateNativeSwapChain();
BuildNativeSwapChain(GetDescriptor().m_dimensions, verticalSyncInterval); CreateSwapchain();
} }
} }
@ -85,46 +84,21 @@ namespace AZ
RHI::DeviceObject::Init(baseDevice); RHI::DeviceObject::Init(baseDevice);
auto& device = static_cast<Device&>(GetDevice()); auto& device = static_cast<Device&>(GetDevice());
RHI::SwapChainDimensions swapchainDimensions = descriptor.m_dimensions; m_dimensions = descriptor.m_dimensions;
result = BuildSurface(descriptor); result = BuildSurface(descriptor);
RETURN_RESULT_IF_UNSUCCESSFUL(result); 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); auto& presentationQueue = device.GetCommandQueueContext().GetOrCreatePresentationCommandQueue(*this);
m_presentationQueue = &presentationQueue; 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 result = CreateSwapchain();
// 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);
RETURN_RESULT_IF_UNSUCCESSFUL(result); RETURN_RESULT_IF_UNSUCCESSFUL(result);
if (nativeDimensions) if (nativeDimensions)
{ {
// Fill out the real swapchain dimensions to return // Fill out the real swapchain dimensions to return
nativeDimensions->m_imageCount = imageCount; *nativeDimensions = m_dimensions;
nativeDimensions->m_imageHeight = swapchainDimensions.m_imageHeight;
nativeDimensions->m_imageWidth = swapchainDimensions.m_imageWidth;
nativeDimensions->m_imageFormat = ConvertFormat(m_surfaceFormat.format); nativeDimensions->m_imageFormat = ConvertFormat(m_surfaceFormat.format);
} }
@ -165,51 +139,24 @@ namespace AZ
RHI::ResultCode SwapChain::ResizeInternal(const RHI::SwapChainDimensions& dimensions, RHI::SwapChainDimensions* nativeDimensions) RHI::ResultCode SwapChain::ResizeInternal(const RHI::SwapChainDimensions& dimensions, RHI::SwapChainDimensions* nativeDimensions)
{ {
auto& device = static_cast<Device&>(GetDevice()); auto& device = static_cast<Device&>(GetDevice());
m_dimensions = dimensions;
InvalidateNativeSwapChain(); 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); auto& presentationQueue = device.GetCommandQueueContext().GetOrCreatePresentationCommandQueue(*this);
m_presentationQueue = &presentationQueue; 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); CreateSwapchain();
// 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);
if (nativeDimensions) if (nativeDimensions)
{ {
*nativeDimensions = resizeDimensions; *nativeDimensions = m_dimensions;
// [ATOM-4840] This is a workaround when the windows is minimized (0x0 size). // [ATOM-4840] This is a workaround when the windows is minimized (0x0 size).
// Add proper support to handle this case. // Add proper support to handle this case.
nativeDimensions->m_imageHeight = AZStd::max(resizeDimensions.m_imageHeight, 1u); nativeDimensions->m_imageHeight = AZStd::max(m_dimensions.m_imageHeight, 1u);
nativeDimensions->m_imageWidth = AZStd::max(resizeDimensions.m_imageWidth, 1u); nativeDimensions->m_imageWidth = AZStd::max(m_dimensions.m_imageWidth, 1u);
nativeDimensions->m_imageFormat = ConvertFormat(m_surfaceFormat.format);
} }
return RHI::ResultCode::Success; return RHI::ResultCode::Success;
@ -271,21 +218,49 @@ namespace AZ
info.pImageIndices = &imageIndex; info.pImageIndices = &imageIndex;
info.pResults = nullptr; info.pResults = nullptr;
[[maybe_unused]] const VkResult result = vkQueuePresentKHR(vulkanQueue->GetNativeQueue(), &info); const VkResult result = vkQueuePresentKHR(vulkanQueue->GetNativeQueue(), &info);
// Resizing window cause recreation of SwapChain after calling this method, // Vulkan's definition of the two types of errors.
// so VK_SUBOPTIMAL_KHR or VK_ERROR_OUT_OF_DATE_KHR should not happen at this point. // VK_ERROR_OUT_OF_DATE_KHR: "A surface has changed in such a way that it is no longer compatible with the swapchain,
AZ_Assert(result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR, "Failed to present swapchain %s", GetName().GetCStr()); // and further presentation requests using the swapchain will fail. Applications must query the new surface
AZ_Warning("Vulkan", result != VK_SUBOPTIMAL_KHR, "Suboptimal presentation of swapchain %s", GetName().GetCStr()); // 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)); m_presentationQueue->QueueCommand(AZStd::move(presentCommand));
uint32_t acquiredImageIndex = GetCurrentImageIndex(); uint32_t acquiredImageIndex = GetCurrentImageIndex();
AcquireNewImage(&acquiredImageIndex); RHI::ResultCode result = AcquireNewImage(&acquiredImageIndex);
if (result == RHI::ResultCode::Fail)
{
InvalidateNativeSwapChain();
CreateSwapchain();
return 0;
}
else
{
return acquiredImageIndex; return acquiredImageIndex;
} }
}
RHI::ResultCode SwapChain::BuildSurface(const RHI::SwapChainDescriptor& descriptor) RHI::ResultCode SwapChain::BuildSurface(const RHI::SwapChainDescriptor& descriptor)
{ {
@ -293,15 +268,8 @@ namespace AZ
surfaceDesc.m_windowHandle = descriptor.m_window; surfaceDesc.m_windowHandle = descriptor.m_window;
RHI::Ptr<WSISurface> surface = WSISurface::Create(); RHI::Ptr<WSISurface> surface = WSISurface::Create();
const RHI::ResultCode result = surface->Init(surfaceDesc); const RHI::ResultCode result = surface->Init(surfaceDesc);
if (result == RHI::ResultCode::Success) RETURN_RESULT_IF_UNSUCCESSFUL(result);
{
m_surface = surface; 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; return result;
} }
@ -373,6 +341,21 @@ namespace AZ
return supportedModes[0]; 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 VkCompositeAlphaFlagBitsKHR SwapChain::GetSupportedCompositeAlpha() const
{ {
VkFlags supportedModesBits = m_surfaceCapabilities.supportedCompositeAlpha; VkFlags supportedModesBits = m_surfaceCapabilities.supportedCompositeAlpha;
@ -394,15 +377,9 @@ namespace AZ
return VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; 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."); 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."); AZ_Assert(m_surface, "Surface is null.");
if (!ValidateSurfaceDimensions(dimensions)) if (!ValidateSurfaceDimensions(dimensions))
@ -410,7 +387,11 @@ namespace AZ
AZ_Assert(false, "Swapchain dimensions are not supported."); AZ_Assert(false, "Swapchain dimensions are not supported.");
return RHI::ResultCode::InvalidArgument; 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 // 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 // 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) // presents the image)
@ -441,11 +422,11 @@ namespace AZ
createInfo.imageArrayLayers = 1; // non-stereoscopic 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.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.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.pQueueFamilyIndices = familyIndices.empty() ? nullptr : familyIndices.data();
createInfo.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; createInfo.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
createInfo.compositeAlpha = GetSupportedCompositeAlpha(); createInfo.compositeAlpha = m_compositeAlphaFlagBits;
createInfo.presentMode = GetSupportedPresentMode(verticalSyncInterval); createInfo.presentMode = m_presentMode;
createInfo.clipped = VK_FALSE; createInfo.clipped = VK_FALSE;
createInfo.oldSwapchain = VK_NULL_HANDLE; createInfo.oldSwapchain = VK_NULL_HANDLE;
@ -467,9 +448,6 @@ namespace AZ
VK_NULL_HANDLE, VK_NULL_HANDLE,
acquiredImageIndex); 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); RHI::ResultCode result = ConvertResult(vkResult);
RETURN_RESULT_IF_UNSUCCESSFUL(result); RETURN_RESULT_IF_UNSUCCESSFUL(result);
@ -484,6 +462,7 @@ namespace AZ
} }
m_currentFrameContext.m_imageAvailableSemaphore = imageAvailableSemaphore; m_currentFrameContext.m_imageAvailableSemaphore = imageAvailableSemaphore;
m_currentFrameContext.m_presentableSemaphore = semaphoreAllocator.Allocate(); m_currentFrameContext.m_presentableSemaphore = semaphoreAllocator.Allocate();
return result; return result;
} }
@ -502,5 +481,70 @@ namespace AZ
m_nativeSwapChain = VK_NULL_HANDLE; 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); 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); 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; 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; 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; 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); RHI::ResultCode AcquireNewImage(uint32_t* acquiredImageIndex);
//! Destroy the surface.
void InvalidateSurface(); void InvalidateSurface();
//! Destroy the old swapchain.
void InvalidateNativeSwapChain(); void InvalidateNativeSwapChain();
VkSwapchainKHR m_nativeSwapChain = VK_NULL_HANDLE;
RHI::Ptr<WSISurface> m_surface; RHI::Ptr<WSISurface> m_surface;
VkSwapchainKHR m_nativeSwapChain = VK_NULL_HANDLE;
CommandQueue* m_presentationQueue = nullptr; CommandQueue* m_presentationQueue = nullptr;
VkSurfaceFormatKHR m_surfaceFormat = {};
VkSurfaceCapabilitiesKHR m_surfaceCapabilities;
FrameContext m_currentFrameContext; 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 struct SwapChainBarrier
{ {
VkPipelineStageFlags m_srcPipelineStages = 0; VkPipelineStageFlags m_srcPipelineStages = 0;
@ -95,8 +119,6 @@ namespace AZ
VkImageMemoryBarrier m_barrier = {}; VkImageMemoryBarrier m_barrier = {};
bool m_isValid = false; bool m_isValid = false;
} m_swapChainBarrier; } m_swapChainBarrier;
AZStd::vector<VkImage> m_swapchainNativeImages;
}; };
} }
} }

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

@ -196,12 +196,8 @@ namespace AtomToolsFramework
bool RenderViewportWidget::event(QEvent* event) 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()) switch (event->type())
{ {
case QEvent::ScreenChangeInternal:
case QEvent::UpdateLater:
case QEvent::Resize: case QEvent::Resize:
SendWindowResizeEvent(); SendWindowResizeEvent();
break; break;

@ -176,6 +176,14 @@ namespace AZ
//! Shadow bias reduces acne by applying a small amount of offset along shadow-space z. //! Shadow bias reduces acne by applying a small amount of offset along shadow-space z.
//! @param Sets the amount of bias to apply. //! @param Sets the amount of bias to apply.
virtual void SetShadowBias(float bias) = 0; 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>; using DirectionalLightRequestBus = EBus<DirectionalLightRequests>;

@ -101,6 +101,9 @@ namespace AZ
//! Method of shadow's filtering. //! Method of shadow's filtering.
ShadowFilterMethod m_shadowFilterMethod = ShadowFilterMethod::None; 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) //! Sample Count for filtering (from 4 to 64)
//! It is used only when the pixel is predicted as on the boundary. //! It is used only when the pixel is predicted as on the boundary.
uint16_t m_filteringSampleCount = 32; uint16_t m_filteringSampleCount = 32;

@ -39,7 +39,8 @@ namespace AZ
->Field("ShadowFilterMethod", &DirectionalLightComponentConfig::m_shadowFilterMethod) ->Field("ShadowFilterMethod", &DirectionalLightComponentConfig::m_shadowFilterMethod)
->Field("PcfFilteringSampleCount", &DirectionalLightComponentConfig::m_filteringSampleCount) ->Field("PcfFilteringSampleCount", &DirectionalLightComponentConfig::m_filteringSampleCount)
->Field("ShadowReceiverPlaneBiasEnabled", &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled) ->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("SetShadowReceiverPlaneBiasEnabled", &DirectionalLightRequestBus::Events::SetShadowReceiverPlaneBiasEnabled)
->Event("GetShadowBias", &DirectionalLightRequestBus::Events::GetShadowBias) ->Event("GetShadowBias", &DirectionalLightRequestBus::Events::GetShadowBias)
->Event("SetShadowBias", &DirectionalLightRequestBus::Events::SetShadowBias) ->Event("SetShadowBias", &DirectionalLightRequestBus::Events::SetShadowBias)
->Event("GetNormalShadowBias", &DirectionalLightRequestBus::Events::GetNormalShadowBias)
->Event("SetNormalShadowBias", &DirectionalLightRequestBus::Events::SetNormalShadowBias)
->VirtualProperty("Color", "GetColor", "SetColor") ->VirtualProperty("Color", "GetColor", "SetColor")
->VirtualProperty("Intensity", "GetIntensity", "SetIntensity") ->VirtualProperty("Intensity", "GetIntensity", "SetIntensity")
->VirtualProperty("AngularDiameter", "GetAngularDiameter", "SetAngularDiameter") ->VirtualProperty("AngularDiameter", "GetAngularDiameter", "SetAngularDiameter")
@ -101,7 +103,8 @@ namespace AZ
->VirtualProperty("ShadowFilterMethod", "GetShadowFilterMethod", "SetShadowFilterMethod") ->VirtualProperty("ShadowFilterMethod", "GetShadowFilterMethod", "SetShadowFilterMethod")
->VirtualProperty("FilteringSampleCount", "GetFilteringSampleCount", "SetFilteringSampleCount") ->VirtualProperty("FilteringSampleCount", "GetFilteringSampleCount", "SetFilteringSampleCount")
->VirtualProperty("ShadowReceiverPlaneBiasEnabled", "GetShadowReceiverPlaneBiasEnabled", "SetShadowReceiverPlaneBiasEnabled") ->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; 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) void DirectionalLightComponentController::SetFilteringSampleCount(uint32_t count)
{ {
const uint16_t count16 = GetMin(Shadow::MaxPcfSamplingCount, aznumeric_cast<uint16_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); SetDebugColoringEnabled(m_configuration.m_isDebugColoringEnabled);
SetShadowFilterMethod(m_configuration.m_shadowFilterMethod); SetShadowFilterMethod(m_configuration.m_shadowFilterMethod);
SetShadowBias(m_configuration.m_shadowBias); SetShadowBias(m_configuration.m_shadowBias);
SetNormalShadowBias(m_configuration.m_normalShadowBias);
SetFilteringSampleCount(m_configuration.m_filteringSampleCount); SetFilteringSampleCount(m_configuration.m_filteringSampleCount);
SetShadowReceiverPlaneBiasEnabled(m_configuration.m_receiverPlaneBiasEnabled); SetShadowReceiverPlaneBiasEnabled(m_configuration.m_receiverPlaneBiasEnabled);

@ -81,7 +81,9 @@ namespace AZ
bool GetShadowReceiverPlaneBiasEnabled() const override; bool GetShadowReceiverPlaneBiasEnabled() const override;
void SetShadowReceiverPlaneBiasEnabled(bool enable) override; void SetShadowReceiverPlaneBiasEnabled(bool enable) override;
float GetShadowBias() const 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: private:
friend class EditorDirectionalLightComponent; friend class EditorDirectionalLightComponent;

@ -136,7 +136,7 @@ namespace AZ
->Attribute(Edit::Attributes::Min, 0.0f) ->Attribute(Edit::Attributes::Min, 0.0f)
->Attribute(Edit::Attributes::Max, 100.0f) ->Attribute(Edit::Attributes::Max, 100.0f)
->Attribute(Edit::Attributes::SoftMin, 0.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::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->Attribute(Edit::Attributes::Visibility, &AreaLightComponentConfig::SupportsShadows) ->Attribute(Edit::Attributes::Visibility, &AreaLightComponentConfig::SupportsShadows)
->Attribute(Edit::Attributes::ReadOnly, &AreaLightComponentConfig::ShadowsDisabled) ->Attribute(Edit::Attributes::ReadOnly, &AreaLightComponentConfig::ShadowsDisabled)

@ -134,7 +134,7 @@ namespace AZ
->EnumAttribute(ShadowFilterMethod::EsmPcf, "ESM+PCF") ->EnumAttribute(ShadowFilterMethod::EsmPcf, "ESM+PCF")
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly)
->DataElement(Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_filteringSampleCount, "Filtering sample count\n", ->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.") "Specific to PCF and ESM+PCF.")
->Attribute(Edit::Attributes::Min, 4) ->Attribute(Edit::Attributes::Min, 4)
->Attribute(Edit::Attributes::Max, 64) ->Attribute(Edit::Attributes::Max, 64)
@ -154,6 +154,13 @@ namespace AZ
->Attribute(Edit::Attributes::Min, 0.f) ->Attribute(Edit::Attributes::Min, 0.f)
->Attribute(Edit::Attributes::Max, 0.2) ->Attribute(Edit::Attributes::Max, 0.2)
->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) ->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") ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_whiteBalanceKelvin, "Temperature", "Temperature in Kelvin")
->Attribute(Edit::Attributes::Min, 1000.0f) ->Attribute(Edit::Attributes::Min, 1000.0f)
->Attribute(Edit::Attributes::Max, 40000.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") ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_whiteBalanceTint, "Tint", "Tint Value")
->Attribute(Edit::Attributes::Min, -100.0f) ->Attribute(Edit::Attributes::Min, -100.0f)
->Attribute(Edit::Attributes::Max, 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") ->ClassElement(AZ::Edit::ClassElements::Group, "Split Toning")
->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->Attribute(AZ::Edit::Attributes::AutoExpand, true)

@ -465,7 +465,7 @@ void ApplyLighting(inout Surface surface, inout LightingData lightingData)
const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight; const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight;
if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount) 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. // Light loops application.

@ -209,6 +209,7 @@ namespace Blast
AZStd::shared_ptr<Physics::Shape>( AZStd::shared_ptr<Physics::Shape>(
const Physics::ColliderConfiguration&, const Physics::ShapeConfiguration&)); const Physics::ColliderConfiguration&, const Physics::ShapeConfiguration&));
MOCK_METHOD1(ReleaseNativeMeshObject, void(void*)); MOCK_METHOD1(ReleaseNativeMeshObject, void(void*));
MOCK_METHOD1(ReleaseNativeHeightfieldObject, void(void*));
MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr<Physics::Material>(const Physics::MaterialConfiguration&)); MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr<Physics::Material>(const Physics::MaterialConfiguration&));
MOCK_METHOD0(GetDefaultMaterial, AZStd::shared_ptr<Physics::Material>()); MOCK_METHOD0(GetDefaultMaterial, AZStd::shared_ptr<Physics::Material>());
MOCK_METHOD1( 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_METHOD2(CreateShape, AZStd::shared_ptr<Physics::Shape>(const Physics::ColliderConfiguration& colliderConfiguration, const Physics::ShapeConfiguration& configuration));
MOCK_METHOD1(ReleaseNativeMeshObject, void(void* nativeMeshObject)); 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_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(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)); MOCK_METHOD3(CookConvexMeshToMemory, bool(const AZ::Vector3* vertices, AZ::u32 vertexCount, AZStd::vector<AZ::u8>& result));

@ -107,6 +107,15 @@ endif()
# Tests # Tests
################################################################################ ################################################################################
if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) 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( ly_add_target(
NAME GradientSignal.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} NAME GradientSignal.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
@ -122,6 +131,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED)
AZ::AzTest AZ::AzTest
Gem::GradientSignal.Static Gem::GradientSignal.Static
Gem::LmbrCentral Gem::LmbrCentral
Gem::GradientSignal.Mocks
) )
ly_add_googletest( ly_add_googletest(
NAME Gem::GradientSignal.Tests 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 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. /// Provide a Component interface for AxisAlignedBoxShape functionality.
class AxisAlignedBoxShapeComponent class AxisAlignedBoxShapeComponent
: public AZ::Component : public AZ::Component

@ -24,6 +24,12 @@ namespace LmbrCentral
/// Type ID for the BoxShapeConfig /// Type ID for the BoxShapeConfig
static const AZ::Uuid BoxShapeConfigTypeId = "{F034FBA2-AC2F-4E66-8152-14DFB90D6283}"; 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 /// Configuration data for BoxShapeComponent
class BoxShapeConfig class BoxShapeConfig
: public ShapeComponentConfig : public ShapeComponentConfig

@ -58,11 +58,10 @@ namespace Multiplayer
void BindNetworkHierarchyLeaveEventHandler(NetworkHierarchyLeaveEvent::Handler& handler) override; void BindNetworkHierarchyLeaveEventHandler(NetworkHierarchyLeaveEvent::Handler& handler) override;
//! @} //! @}
protected: private:
//! Used by @NetworkHierarchyRootComponent //! Used by @NetworkHierarchyRootComponent
void SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot); void SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot);
private:
AZ::ChildChangedEvent::Handler m_childChangedHandler; AZ::ChildChangedEvent::Handler m_childChangedHandler;
void OnChildChanged(AZ::ChildChangeType type, AZ::EntityId child); void OnChildChanged(AZ::ChildChangeType type, AZ::EntityId child);
@ -80,5 +79,8 @@ namespace Multiplayer
bool m_isHierarchyEnabled = true; bool m_isHierarchyEnabled = true;
void NotifyChildrenHierarchyDisbanded(); 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); bool SerializeEntityCorrection(AzNetworking::ISerializer& serializer);
protected:
void SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot);
private: private:
void SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot);
AZ::ChildChangedEvent::Handler m_childChangedHandler; AZ::ChildChangedEvent::Handler m_childChangedHandler;
AZ::ParentChangedEvent::Handler m_parentChangedHandler; AZ::ParentChangedEvent::Handler m_parentChangedHandler;
@ -85,11 +84,14 @@ namespace Multiplayer
//! Builds the hierarchy using breadth-first iterative method. //! Builds the hierarchy using breadth-first iterative method.
void InternalBuildHierarchyList(AZ::Entity* underEntity); 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. //! Set to false when deactivating or otherwise not to be included in hierarchy considerations.
bool m_isHierarchyEnabled = true; bool m_isHierarchyEnabled = true;
AzNetworking::ConnectionId m_previousOwningConnectionId = AzNetworking::InvalidConnectionId;
void SetOwningConnectionId(AzNetworking::ConnectionId connectionId) override;
friend class HierarchyBenchmarkBase; friend class HierarchyBenchmarkBase;
}; };

@ -129,34 +129,48 @@ namespace Multiplayer
handler.Connect(m_networkHierarchyLeaveEvent); 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 (m_rootEntity != newHierarchyRoot)
{
m_rootEntity = newHierarchyRoot;
if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority)
{ {
NetworkHierarchyChildComponentController* controller = static_cast<NetworkHierarchyChildComponentController*>(GetController()); NetworkHierarchyChildComponentController* controller = static_cast<NetworkHierarchyChildComponentController*>(GetController());
if (m_rootEntity)
{
const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId()); const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId());
controller->SetHierarchyRoot(netRootId); controller->SetHierarchyRoot(netRootId);
}
GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent<NetBindComponent>()->GetOwningConnectionId());
m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId());
} }
else }
else if ((previousHierarchyRoot && m_rootEntity == previousHierarchyRoot) || !previousHierarchyRoot)
{
m_rootEntity = nullptr;
if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority)
{ {
NetworkHierarchyChildComponentController* controller = static_cast<NetworkHierarchyChildComponentController*>(GetController());
controller->SetHierarchyRoot(InvalidNetEntityId); controller->SetHierarchyRoot(InvalidNetEntityId);
}
GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId);
m_networkHierarchyLeaveEvent.Signal(); m_networkHierarchyLeaveEvent.Signal();
NotifyChildrenHierarchyDisbanded();
} }
} }
if (m_rootEntity == nullptr) void NetworkHierarchyChildComponent::SetOwningConnectionId(AzNetworking::ConnectionId connectionId)
{ {
NotifyChildrenHierarchyDisbanded(); NetworkHierarchyChildComponentBase::SetOwningConnectionId(connectionId);
} if (IsHierarchicalChild() == false)
{
m_previousOwningConnectionId = connectionId;
} }
} }
@ -180,14 +194,18 @@ namespace Multiplayer
if (m_rootEntity != newRoot) if (m_rootEntity != newRoot)
{ {
m_rootEntity = newRoot; m_rootEntity = newRoot;
m_previousOwningConnectionId = GetNetBindComponent()->GetOwningConnectionId();
GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent<NetBindComponent>()->GetOwningConnectionId());
m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId());
} }
} }
else else
{ {
GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId);
m_isHierarchyEnabled = false; m_isHierarchyEnabled = false;
m_rootEntity = nullptr; m_rootEntity = nullptr;
m_networkHierarchyLeaveEvent.Signal();
} }
} }
@ -203,11 +221,11 @@ namespace Multiplayer
{ {
if (auto* hierarchyChildComponent = childEntity->FindComponent<NetworkHierarchyChildComponent>()) if (auto* hierarchyChildComponent = childEntity->FindComponent<NetworkHierarchyChildComponent>())
{ {
hierarchyChildComponent->SetTopLevelHierarchyRootEntity(nullptr); hierarchyChildComponent->SetTopLevelHierarchyRootEntity(nullptr, nullptr);
} }
else if (auto* hierarchyRootComponent = childEntity->FindComponent<NetworkHierarchyRootComponent>()) 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)) 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); auto [rootComponent, childComponent] = GetHierarchyComponents(parentEntity);
if (rootComponent == nullptr && childComponent == nullptr) if (rootComponent == nullptr && childComponent == nullptr)
{ {
RebuildHierarchy(); SetRootForEntity(nullptr, nullptr, GetEntity());
} }
else else
{ {
@ -219,7 +219,7 @@ namespace Multiplayer
else else
{ {
// Detached from parent // Detached from parent
RebuildHierarchy(); SetRootForEntity(nullptr, nullptr, GetEntity());
} }
} }
@ -247,14 +247,14 @@ namespace Multiplayer
{ {
// This is a newly added entity to the network hierarchy. // This is a newly added entity to the network hierarchy.
hierarchyChanged = true; hierarchyChanged = true;
SetRootForEntity(GetEntity(), currentEntity); SetRootForEntity(nullptr, GetEntity(), currentEntity);
} }
} }
// These entities were removed since last rebuild. // These entities were removed since last rebuild.
for (const AZ::Entity* previousEntity : previousEntities) for (const AZ::Entity* previousEntity : previousEntities)
{ {
SetRootForEntity(nullptr, previousEntity); SetRootForEntity(GetEntity(), nullptr, previousEntity);
} }
if (!previousEntities.empty()) 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); auto [hierarchyRootComponent, hierarchyChildComponent] = GetHierarchyComponents(childEntity);
if (hierarchyChildComponent) if (hierarchyChildComponent)
{ {
hierarchyChildComponent->SetTopLevelHierarchyRootEntity(root); hierarchyChildComponent->SetTopLevelHierarchyRootEntity(previousKnownRoot, newRoot);
} }
else if (hierarchyRootComponent) 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)
{
if (newHierarchyRoot)
{ {
m_rootEntity = hierarchyRoot; if (m_rootEntity != newHierarchyRoot)
{
m_rootEntity = newHierarchyRoot;
if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority)
{ {
NetworkHierarchyChildComponentController* controller = static_cast<NetworkHierarchyChildComponentController*>(GetController()); NetworkHierarchyRootComponentController* controller = static_cast<NetworkHierarchyRootComponentController*>(GetController());
if (hierarchyRoot) const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId());
{
const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(hierarchyRoot->GetId());
controller->SetHierarchyRoot(netRootId); controller->SetHierarchyRoot(netRootId);
} }
else
{ GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent<NetBindComponent>()->GetOwningConnectionId());
controller->SetHierarchyRoot(InvalidNetEntityId); 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)
{ {
NetworkHierarchyRootComponentController* controller = static_cast<NetworkHierarchyRootComponentController*>(GetController());
controller->SetHierarchyRoot(InvalidNetEntityId);
}
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. // We lost the parent hierarchical entity, so as a root we need to re-build our own hierarchy.
RebuildHierarchy(); RebuildHierarchy();
} }
} }
void NetworkHierarchyRootComponent::SetOwningConnectionId(AzNetworking::ConnectionId connectionId)
{
NetworkHierarchyRootComponentBase::SetOwningConnectionId(connectionId);
if (IsHierarchicalChild() == false)
{
m_previousOwningConnectionId = connectionId;
}
}
NetworkHierarchyRootComponentController::NetworkHierarchyRootComponentController(NetworkHierarchyRootComponent& parent) NetworkHierarchyRootComponentController::NetworkHierarchyRootComponentController(NetworkHierarchyRootComponent& parent)
: NetworkHierarchyRootComponentControllerBase(parent) : NetworkHierarchyRootComponentControllerBase(parent)
{ {

@ -302,6 +302,36 @@ namespace Multiplayer
SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId); 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 * Parent -> Child -> ChildOfChild
*/ */

@ -195,6 +195,49 @@ namespace Multiplayer
m_child->m_entity.reset(); 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 * Parent -> Child -> ChildOfChild
*/ */
@ -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) 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()); 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); 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) * 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_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()); m_root3->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(m_childOfChild->m_entity->GetId());
MockNetworkHierarchyCallbackHandler mock; MockNetworkHierarchyCallbackHandler mock;
EXPECT_CALL(mock, OnNetworkHierarchyLeave()); EXPECT_CALL(mock, OnNetworkHierarchyUpdated(m_root3->m_entity->GetId()));
m_childOfChild3->m_entity->FindComponent<NetworkHierarchyChildComponent>()->BindNetworkHierarchyLeaveEventHandler(mock.m_leaveHandler); m_childOfChild3->m_entity->FindComponent<NetworkHierarchyChildComponent>()->BindNetworkHierarchyChangedEventHandler(mock.m_changedHandler);
m_child->m_entity->FindComponent<AzFramework::TransformComponent>()->SetParent(AZ::EntityId()); 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 const AZ::Vector3& colliderScale) const
{ {
// Apply entity world transform scale to collider offset // Apply entity world transform scale to collider offset

@ -104,7 +104,15 @@ namespace PhysX
const AZ::Vector3& meshScale, const AZ::Vector3& meshScale,
AZ::u32 geomIndex) const; 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; const Physics::ColliderConfiguration& colliderConfig, const AZStd::vector<AZ::Vector3>& points) const;
AZ::Transform GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig, AZ::Transform GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig,

@ -16,19 +16,21 @@ namespace AzPhysics
{ {
class CollisionGroup; class CollisionGroup;
class CollisionLayer; class CollisionLayer;
} } // namespace AzPhysics
namespace physx namespace physx
{ {
class PxScene; class PxScene;
class PxSceneDesc; class PxSceneDesc;
class PxConvexMesh; class PxConvexMesh;
class PxHeightField;
class PxTriangleMesh; class PxTriangleMesh;
class PxShape; class PxShape;
class PxCooking; class PxCooking;
class PxControllerManager; class PxControllerManager;
struct PxFilterData; struct PxFilterData;
} struct PxHeightFieldSample;
} // namespace physx
namespace PhysX namespace PhysX
{ {
@ -63,6 +65,13 @@ namespace PhysX
/// @return Pointer to the created mesh. /// @return Pointer to the created mesh.
virtual physx::PxTriangleMesh* CreateTriangleMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) = 0; 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. /// Creates PhysX collision filter data from generic collision filtering settings.
/// @param layer The collision layer the object belongs to. /// @param layer The collision layer the object belongs to.
/// @param group The set of collision layers the object will interact with. /// @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