From 961c41217983f6bab96552a3731cbc637c2d6150 Mon Sep 17 00:00:00 2001 From: AMZN-Phil Date: Wed, 20 Oct 2021 16:39:11 -0700 Subject: [PATCH 01/10] Fix adding local repos due to JSON not being able to serialize Windows paths Signed-off-by: AMZN-Phil --- scripts/o3de/o3de/register.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/o3de/o3de/register.py b/scripts/o3de/o3de/register.py index c3d201f23c..3c4d69a3c0 100644 --- a/scripts/o3de/o3de/register.py +++ b/scripts/o3de/o3de/register.py @@ -480,9 +480,9 @@ def register_repo(json_data: dict, while repo_uri in json_data['repos']: json_data['repos'].remove(repo_uri) else: - repo_uri = pathlib.Path(repo_uri).resolve() - while repo_uri.as_posix() in json_data['repos']: - json_data['repos'].remove(repo_uri.as_posix()) + repo_uri = pathlib.Path(repo_uri).resolve().as_posix() + while repo_uri in json_data['repos']: + json_data['repos'].remove(repo_uri) if remove: logger.warn(f'Removing repo uri {repo_uri}.') From d78aa5bf905231ed954066a22b69fa1ec9772f34 Mon Sep 17 00:00:00 2001 From: Chris Galvan Date: Thu, 21 Oct 2021 10:55:32 -0500 Subject: [PATCH 02/10] Removed legacy ColorGradientCtrl. Signed-off-by: Chris Galvan --- Code/Editor/Controls/ColorGradientCtrl.cpp | 923 ------------------ Code/Editor/Controls/ColorGradientCtrl.h | 167 ---- .../ReflectedPropertyControl/PropertyCtrl.cpp | 1 - .../PropertyMiscCtrl.cpp | 27 - .../PropertyMiscCtrl.h | 14 - Code/Editor/editor_lib_files.cmake | 2 - 6 files changed, 1134 deletions(-) delete mode 100644 Code/Editor/Controls/ColorGradientCtrl.cpp delete mode 100644 Code/Editor/Controls/ColorGradientCtrl.h diff --git a/Code/Editor/Controls/ColorGradientCtrl.cpp b/Code/Editor/Controls/ColorGradientCtrl.cpp deleted file mode 100644 index 446e5810c5..0000000000 --- a/Code/Editor/Controls/ColorGradientCtrl.cpp +++ /dev/null @@ -1,923 +0,0 @@ -/* - * 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 "EditorDefs.h" - -#include "ColorGradientCtrl.h" - -// Qt -#include -#include - -// AzQtComponents -#include - - -#define MIN_TIME_EPSILON 0.01f - -////////////////////////////////////////////////////////////////////////// -CColorGradientCtrl::CColorGradientCtrl(QWidget* parent) - : QWidget(parent) -{ - m_nActiveKey = -1; - m_nHitKeyIndex = -1; - m_nKeyDrawRadius = 3; - m_bTracking = false; - m_pSpline = nullptr; - m_fMinTime = -1; - m_fMaxTime = 1; - m_fMinValue = -1; - m_fMaxValue = 1; - m_fTooltipScaleX = 1; - m_fTooltipScaleY = 1; - m_bNoTimeMarker = true; - m_bLockFirstLastKey = false; - m_bNoZoom = true; - - ClearSelection(); - - m_bSelectedKeys.reserve(0); - - m_fTimeMarker = -10; - - m_grid.zoom.x = 100; - - setMouseTracking(true); -} - -CColorGradientCtrl::~CColorGradientCtrl() -{ -} - - -///////////////////////////////////////////////////////////////////////////// -// QColorGradientCtrl message handlers - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::resizeEvent(QResizeEvent* event) -{ - QWidget::resizeEvent(event); - - QRect rc(QPoint(0, 0), event->size()); - m_rcGradient = rc; - m_rcGradient.setHeight(m_rcGradient.height() - 11); - //m_rcGradient.DeflateRect(4,4); - - m_grid.rect = m_rcGradient; - if (m_bNoZoom) - { - m_grid.zoom.x = static_cast(m_grid.rect.width()); - } - - m_rcKeys = rc; - m_rcKeys.setTop(m_rcKeys.bottom() - 10); -} - - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetZoom(float fZoom) -{ - m_grid.zoom.x = fZoom; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetOrigin(float fOffset) -{ - m_grid.origin.x = fOffset; -} - -////////////////////////////////////////////////////////////////////////// -QPoint CColorGradientCtrl::KeyToPoint(int nKey) -{ - if (nKey >= 0) - { - return TimeToPoint(m_pSpline->GetKeyTime(nKey)); - } - return QPoint(0, 0); -} - -////////////////////////////////////////////////////////////////////////// -QPoint CColorGradientCtrl::TimeToPoint(float time) -{ - return QPoint(m_grid.WorldToClient(Vec2(time, 0)).x(), m_rcGradient.height() / 2); -} - -////////////////////////////////////////////////////////////////////////// -AZ::Color CColorGradientCtrl::TimeToColor(float time) -{ - ISplineInterpolator::ValueType val; - m_pSpline->Interpolate(time, val); - const AZ::Color col = ValueToColor(val); - return col; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::PointToTimeValue(QPoint point, float& time, ISplineInterpolator::ValueType& val) -{ - time = XOfsToTime(point.x()); - ColorToValue(TimeToColor(time), val); -} - -////////////////////////////////////////////////////////////////////////// -float CColorGradientCtrl::XOfsToTime(int x) -{ - return m_grid.ClientToWorld(QPoint(x, 0)).x; -} - -////////////////////////////////////////////////////////////////////////// -QPoint CColorGradientCtrl::XOfsToPoint(int x) -{ - return TimeToPoint(XOfsToTime(x)); -} - -////////////////////////////////////////////////////////////////////////// -AZ::Color CColorGradientCtrl::XOfsToColor(int x) -{ - return TimeToColor(XOfsToTime(x)); -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::paintEvent(QPaintEvent* e) -{ - QPainter painter(this); - - QRect rcClient = rect(); - - if (m_pSpline) - { - m_bSelectedKeys.resize(m_pSpline->GetKeyCount()); - } - { - if (!isEnabled()) - { - painter.setBrush(palette().button()); - painter.drawRect(rcClient); - return; - } - - ////////////////////////////////////////////////////////////////////////// - // Fill keys backgound. - ////////////////////////////////////////////////////////////////////////// - QRect rcKeys = m_rcKeys.intersected(e->rect()); - painter.setBrush(palette().button()); - painter.drawRect(rcKeys); - ////////////////////////////////////////////////////////////////////////// - - //Draw Keys and Curve - if (m_pSpline) - { - DrawGradient(e, &painter); - DrawKeys(e, &painter); - } - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::DrawGradient(QPaintEvent* e, QPainter* painter) -{ - //Draw Curve - // create and select a thick, white pen - painter->setPen(QPen(QColor(128, 255, 128), 1, Qt::SolidLine)); - - const QRect rcClip = e->rect().intersected(m_rcGradient); - const int right = rcClip.left() + rcClip.width(); - for (int x = rcClip.left(); x < right; x++) - { - const AZ::Color col = XOfsToColor(x); - QPen pen(QColor(col.GetR8(), col.GetG8(), col.GetR8(), col.GetA8()), 1, Qt::SolidLine); - painter->setPen(pen); - painter->drawLine(x, m_rcGradient.top(), x, m_rcGradient.top() + m_rcGradient.height()); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::DrawKeys(QPaintEvent* e, QPainter* painter) -{ - if (!m_pSpline) - { - return; - } - - // create and select a white pen - painter->setPen(QPen(QColor(0, 0, 0), 1, Qt::SolidLine)); - - QRect rcClip = e->rect(); - - m_bSelectedKeys.resize(m_pSpline->GetKeyCount()); - - for (int i = 0; i < m_pSpline->GetKeyCount(); i++) - { - float time = m_pSpline->GetKeyTime(i); - QPoint pt = TimeToPoint(time); - - if (pt.x() < rcClip.left() - 8 || pt.x() > rcClip.left() + rcClip.width() + 8) - { - continue; - } - - const AZ::Color clr = TimeToColor(time); - QBrush brush(QColor(clr.GetR8(), clr.GetG8(), clr.GetB8(), clr.GetA8())); - painter->setBrush(brush); - - // Find the midpoints of the top, right, left, and bottom - // of the client area. They will be the vertices of our polygon. - QPoint pts[3]; - pts[0].rx() = pt.x(); - pts[0].ry() = m_rcKeys.top() + 1; - pts[1].rx() = pt.x() - 5; - pts[1].ry() = m_rcKeys.top() + 8; - pts[2].rx() = pt.x() + 5; - pts[2].ry() = m_rcKeys.top() + 8; - painter->drawPolygon(pts, 3); - - if (m_bSelectedKeys[i]) - { - QPen pen(QColor(200, 0, 0), 1, Qt::SolidLine); - QPen oldPen = painter->pen(); - painter->setPen(pen); - painter->drawPolygon(pts, 3); - painter->setPen(oldPen); - } - } - - if (!m_bNoTimeMarker) - { - QPen timePen(QColor(255, 0, 255), 1, Qt::SolidLine); - painter->setPen(timePen); - QPoint pt = TimeToPoint(m_fTimeMarker); - painter->drawLine(pt.x(), m_rcGradient.top() + 1, pt.x(), m_rcGradient.bottom() - 1); - } -} - -void CColorGradientCtrl::UpdateTooltip(QPoint pos) -{ - if (m_nHitKeyIndex >= 0) - { - float time = m_pSpline->GetKeyTime(m_nHitKeyIndex); - ISplineInterpolator::ValueType val; - m_pSpline->GetKeyValue(m_nHitKeyIndex, val); - - AZ::Color col = TimeToColor(time); - int cont_s = (m_pSpline->GetKeyFlags(m_nHitKeyIndex) >> SPLINE_KEY_TANGENT_IN_SHIFT) & SPLINE_KEY_TANGENT_LINEAR ? 1 : 2; - int cont_d = (m_pSpline->GetKeyFlags(m_nHitKeyIndex) >> SPLINE_KEY_TANGENT_OUT_SHIFT) & SPLINE_KEY_TANGENT_LINEAR ? 1 : 2; - - QString tipText(tr("%1 : %2,%3,%4 [%5,%6]").arg(time * m_fTooltipScaleX, 0, 'f', 2).arg(col.GetR8()).arg(col.GetG8()).arg(col.GetB8()).arg(cont_s).arg(cont_d)); - const QPoint globalPos = mapToGlobal(pos); - QToolTip::showText(mapToGlobal(pos), tipText, this, QRect(globalPos, QSize(1, 1))); - } -} - -///////////////////////////////////////////////////////////////////////////// -//Mouse Message Handlers -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::mousePressEvent(QMouseEvent* event) -{ - if (event->button() == Qt::LeftButton) - { - OnLButtonDown(event); - } - else if (event->button() == Qt::RightButton) - { - OnRButtonDown(event); - } -} - -void CColorGradientCtrl::OnLButtonDown([[maybe_unused]] QMouseEvent* event) -{ - if (m_bTracking) - { - return; - } - if (!m_pSpline) - { - return; - } - - setFocus(); - - switch (m_hitCode) - { - case HIT_KEY: - StartTracking(); - SetActiveKey(m_nHitKeyIndex); - break; - - /* - case HIT_SPLINE: - { - // Cycle the spline slope of the nearest key. - int flags = m_pSpline->GetKeyFlags(m_nHitKeyIndex); - if (m_nHitKeyDist < 0) - // Toggle left side. - flags ^= SPLINE_KEY_TANGENT_LINEAR << SPLINE_KEY_TANGENT_IN_SHIFT; - if (m_nHitKeyDist > 0) - // Toggle right side. - flags ^= SPLINE_KEY_TANGENT_LINEAR << SPLINE_KEY_TANGENT_OUT_SHIFT; - m_pSpline->SetKeyFlags(m_nHitKeyIndex, flags); - m_pSpline->Update(); - - SetActiveKey(-1); - SendNotifyEvent( CLRGRDN_CHANGE ); - if (m_updateCallback) - m_updateCallback(this); - break; - } - */ - - case HIT_NOTHING: - SetActiveKey(-1); - break; - } - update(); -} - - - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::OnRButtonDown([[maybe_unused]] QMouseEvent* event) -{ -} - - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::mouseDoubleClickEvent(QMouseEvent* event) -{ - if (!m_pSpline) - { - return; - } - - if (event->button() != Qt::LeftButton) - { - return; - } - - switch (m_hitCode) - { - case HIT_SPLINE: - { - int iIndex = InsertKey(event->pos()); - SetActiveKey(iIndex); - EditKey(iIndex); - - update(); - } - break; - case HIT_KEY: - { - EditKey(m_nHitKeyIndex); - } - break; - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::mouseMoveEvent(QMouseEvent* event) -{ - if (!m_pSpline) - { - return; - } - - if (!m_bTracking) - { - switch (HitTest(event->pos())) - { - case HIT_SPLINE: - { - setCursor(CMFCUtils::LoadCursor(IDC_ARRWHITE)); - } break; - case HIT_KEY: - { - setCursor(CMFCUtils::LoadCursor(IDC_ARRBLCK)); - } break; - default: - break; - } - } - - if (m_bTracking) - { - TrackKey(event->pos()); - } - - if (m_bTracking || m_nHitKeyIndex >= 0) - { - UpdateTooltip(event->pos()); - } - else - { - QToolTip::hideText(); - } -} - -void CColorGradientCtrl::mouseReleaseEvent(QMouseEvent* event) -{ - if (event->button() == Qt::LeftButton) - { - OnLButtonUp(event); - } - else if (event->button() == Qt::RightButton) - { - OnRButtonUp(event); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::OnLButtonUp(QMouseEvent* event) -{ - if (!m_pSpline) - { - return; - } - - if (m_bTracking) - { - StopTracking(event->pos()); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::OnRButtonUp([[maybe_unused]] QMouseEvent* event) -{ - if (!m_pSpline) - { - return; - } -} - -///////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetActiveKey(int nIndex) -{ - ClearSelection(); - - //Activate New Key - if (nIndex >= 0) - { - m_bSelectedKeys[nIndex] = true; - } - m_nActiveKey = nIndex; - update(); - - SendNotifyEvent(CLRGRDN_ACTIVE_KEY_CHANGE); -} - -///////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetSpline(ISplineInterpolator* pSpline, bool bRedraw) -{ - if (pSpline != m_pSpline) - { - //if (pSpline && pSpline->GetNumDimensions() != 3) - //return; - m_pSpline = pSpline; - m_nActiveKey = -1; - } - - ClearSelection(); - - if (bRedraw) - { - update(); - } -} - -////////////////////////////////////////////////////////////////////////// -ISplineInterpolator* CColorGradientCtrl::GetSpline() -{ - return m_pSpline; -} - -///////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::keyPressEvent(QKeyEvent* event) -{ - bool bProcessed = false; - - if (m_nActiveKey != -1 && m_pSpline) - { - switch (event->key()) - { - case Qt::Key_Delete: - { - RemoveKey(m_nActiveKey); - bProcessed = true; - } break; - case Qt::Key_Up: - { - CUndo undo("Move Spline Key"); - QPoint point = KeyToPoint(m_nActiveKey); - point.rx() -= 1; - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - TrackKey(point); - bProcessed = true; - } break; - case Qt::Key_Down: - { - CUndo undo("Move Spline Key"); - QPoint point = KeyToPoint(m_nActiveKey); - point.rx() += 1; - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - TrackKey(point); - bProcessed = true; - } break; - case Qt::Key_Left: - { - CUndo undo("Move Spline Key"); - QPoint point = KeyToPoint(m_nActiveKey); - point.rx() -= 1; - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - TrackKey(point); - bProcessed = true; - } break; - case Qt::Key_Right: - { - CUndo undo("Move Spline Key"); - QPoint point = KeyToPoint(m_nActiveKey); - point.rx() += 1; - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - TrackKey(point); - bProcessed = true; - } break; - - default: - break; //do nothing - } - - update(); - } - - event->setAccepted(bProcessed); -} - -////////////////////////////////////////////////////////////////////////////// -CColorGradientCtrl::EHitCode CColorGradientCtrl::HitTest(QPoint point) -{ - if (!m_pSpline) - { - return HIT_NOTHING; - } - - ISplineInterpolator::ValueType val; - float time; - PointToTimeValue(point, time, val); - - QRect rc = rect(); - - m_nHitKeyIndex = -1; - - if (rc.contains(point)) - { - m_nHitKeyDist = 0xFFFF; - m_hitCode = HIT_SPLINE; - - for (int i = 0; i < m_pSpline->GetKeyCount(); i++) - { - QPoint splinePt = TimeToPoint(m_pSpline->GetKeyTime(i)); - if (abs(point.x() - splinePt.x()) < abs(m_nHitKeyDist)) - { - m_nHitKeyIndex = i; - m_nHitKeyDist = point.x() - splinePt.x(); - } - } - if (abs(m_nHitKeyDist) < 4) - { - m_hitCode = HIT_KEY; - } - } - else - { - m_hitCode = HIT_NOTHING; - } - - return m_hitCode; -} - -/////////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::StartTracking() -{ - m_bTracking = true; - - GetIEditor()->BeginUndo(); - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - - setCursor(CMFCUtils::LoadCursor(IDC_ARRBLCKCROSS)); -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::TrackKey(QPoint point) -{ - if (point.x() < m_rcGradient.left() || point.y() > m_rcGradient.right()) - { - return; - } - - int nKey = m_nHitKeyIndex; - - if (nKey >= 0) - { - ISplineInterpolator::ValueType val; - float time; - PointToTimeValue(point, time, val); - - // Clamp to min/max time. - if (time < m_fMinTime || time > m_fMaxTime) - { - return; - } - - int i; - for (i = 0; i < m_pSpline->GetKeyCount(); i++) - { - // Switch to next key. - if ((m_pSpline->GetKeyTime(i) < time && i > nKey) || - (m_pSpline->GetKeyTime(i) > time && i < nKey)) - { - m_pSpline->SetKeyTime(nKey, time); - m_pSpline->Update(); - SetActiveKey(i); - m_nHitKeyIndex = i; - return; - } - } - - if (!m_bLockFirstLastKey || (nKey != 0 && nKey != m_pSpline->GetKeyCount() - 1)) - { - m_pSpline->SetKeyTime(nKey, time); - m_pSpline->Update(); - } - - SendNotifyEvent(CLRGRDN_CHANGE); - if (m_updateCallback) - { - m_updateCallback(this); - } - - update(); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::StopTracking(QPoint point) -{ - if (!m_bTracking) - { - return; - } - - GetIEditor()->AcceptUndo("Spline Move"); - - if (m_nHitKeyIndex >= 0) - { - QRect rc = rect(); - rc = rc.marginsAdded(QMargins(100, 100, 100, 100)); - if (!rc.contains(point)) - { - RemoveKey(m_nHitKeyIndex); - } - } - - m_bTracking = false; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::EditKey(int nKey) -{ - if (!m_pSpline) - { - return; - } - - if (nKey < 0 || nKey >= m_pSpline->GetKeyCount()) - { - return; - } - - SetActiveKey(nKey); - - ISplineInterpolator::ValueType val; - m_pSpline->GetKeyValue(nKey, val); - - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - - AzQtComponents::ColorPicker dlg(AzQtComponents::ColorPicker::Configuration::RGB); - dlg.setCurrentColor(ValueToColor(val)); - dlg.setSelectedColor(ValueToColor(val)); - connect(&dlg, &AzQtComponents::ColorPicker::currentColorChanged, this, &CColorGradientCtrl::OnKeyColorChanged); - if (dlg.exec() == QDialog::Accepted) - { - CUndo undo("Modify Gradient Color"); - OnKeyColorChanged(dlg.selectedColor()); - } - else - { - OnKeyColorChanged(ValueToColor(val)); - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::OnKeyColorChanged(const AZ::Color& color) -{ - int nKey = m_nActiveKey; - if (!m_pSpline) - { - return; - } - if (nKey < 0 || nKey >= m_pSpline->GetKeyCount()) - { - return; - } - - ISplineInterpolator::ValueType val; - ColorToValue(color, val); - m_pSpline->SetKeyValue(nKey, val); - update(); - - if (m_bLockFirstLastKey) - { - if (nKey == 0) - { - m_pSpline->SetKeyValue(m_pSpline->GetKeyCount() - 1, val); - } - else if (nKey == m_pSpline->GetKeyCount() - 1) - { - m_pSpline->SetKeyValue(0, val); - } - } - m_pSpline->Update(); - SendNotifyEvent(CLRGRDN_CHANGE); - if (m_updateCallback) - { - m_updateCallback(this); - } - - GetIEditor()->UpdateViews(eRedrawViewports); -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::RemoveKey(int nKey) -{ - if (!m_pSpline) - { - return; - } - if (m_bLockFirstLastKey) - { - if (nKey == 0 || nKey == m_pSpline->GetKeyCount() - 1) - { - return; - } - } - - CUndo undo("Remove Spline Key"); - - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - m_nActiveKey = -1; - m_nHitKeyIndex = -1; - if (m_pSpline) - { - m_pSpline->RemoveKey(nKey); - m_pSpline->Update(); - } - SendNotifyEvent(CLRGRDN_CHANGE); - if (m_updateCallback) - { - m_updateCallback(this); - } - - update(); -} - -////////////////////////////////////////////////////////////////////////// -int CColorGradientCtrl::InsertKey(QPoint point) -{ - CUndo undo("Spline Insert Key"); - - ISplineInterpolator::ValueType val; - - float time; - PointToTimeValue(point, time, val); - - if (time < m_fMinTime || time > m_fMaxTime) - { - return -1; - } - - int i; - for (i = 0; i < m_pSpline->GetKeyCount(); i++) - { - // Skip if any key already have time that is very close. - if (fabs(m_pSpline->GetKeyTime(i) - time) < MIN_TIME_EPSILON) - { - return i; - } - } - - SendNotifyEvent(CLRGRDN_BEFORE_CHANGE); - - m_pSpline->InsertKey(time, val); - m_pSpline->Interpolate(time, val); - ClearSelection(); - update(); - - SendNotifyEvent(CLRGRDN_CHANGE); - if (m_updateCallback) - { - m_updateCallback(this); - } - - for (i = 0; i < m_pSpline->GetKeyCount(); i++) - { - // Find key with added time. - if (m_pSpline->GetKeyTime(i) == time) - { - return i; - } - } - - return -1; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::ClearSelection() -{ - m_nActiveKey = -1; - if (m_pSpline) - { - m_bSelectedKeys.resize(m_pSpline->GetKeyCount()); - } - for (int i = 0; i < (int)m_bSelectedKeys.size(); i++) - { - m_bSelectedKeys[i] = false; - } -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SetTimeMarker(float fTime) -{ - if (!m_pSpline) - { - return; - } - - { - QPoint pt = TimeToPoint(m_fTimeMarker); - QRect rc = QRect(pt.x(), m_rcGradient.top(), 0, m_rcGradient.bottom() - m_rcGradient.top()).normalized(); - rc += QMargins(1, 0, 1, 0); - update(rc); - } - { - QPoint pt = TimeToPoint(fTime); - QRect rc = QRect(pt.x(), m_rcGradient.top(), 0, m_rcGradient.bottom() - m_rcGradient.top()).normalized(); - rc += QMargins(1, 0, 1, 0); - update(rc); - } - m_fTimeMarker = fTime; -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::SendNotifyEvent(int nEvent) -{ - switch (nEvent) - { - case CLRGRDN_BEFORE_CHANGE: - emit beforeChange(); - break; - case CLRGRDN_CHANGE: - emit change(); - break; - case CLRGRDN_ACTIVE_KEY_CHANGE: - emit activeKeyChange(); - break; - } -} - -////////////////////////////////////////////////////////////////////////// -AZ::Color CColorGradientCtrl::ValueToColor(ISplineInterpolator::ValueType val) -{ - const AZ::Color color(val[0], val[1], val[2], 1.0); - return color.LinearToGamma(); -} - -////////////////////////////////////////////////////////////////////////// -void CColorGradientCtrl::ColorToValue(const AZ::Color& col, ISplineInterpolator::ValueType& val) -{ - const AZ::Color colLin = col.GammaToLinear(); - val[0] = colLin.GetR(); - val[1] = colLin.GetG(); - val[2] = colLin.GetB(); - val[3] = 0; -} - -void CColorGradientCtrl::SetNoTimeMarker(bool noTimeMarker) -{ - m_bNoTimeMarker = noTimeMarker; - update(); -} - - -#include diff --git a/Code/Editor/Controls/ColorGradientCtrl.h b/Code/Editor/Controls/ColorGradientCtrl.h deleted file mode 100644 index bb1a83b0c1..0000000000 --- a/Code/Editor/Controls/ColorGradientCtrl.h +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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 - * - */ - - -#ifndef CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H -#define CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H -#pragma once - -#if !defined(Q_MOC_RUN) -#include -#include -#include "Controls/WndGridHelper.h" -#endif - -namespace AZ -{ - class Color; -} - -// Notify event sent when spline is being modified. -#define CLRGRDN_CHANGE (0x0001) -// Notify event sent just before when spline is modified. -#define CLRGRDN_BEFORE_CHANGE (0x0002) -// Notify event sent when the active key changes -#define CLRGRDN_ACTIVE_KEY_CHANGE (0x0003) - -////////////////////////////////////////////////////////////////////////// -// Spline control. -////////////////////////////////////////////////////////////////////////// -class CColorGradientCtrl - : public QWidget -{ - Q_OBJECT -public: - CColorGradientCtrl(QWidget* parent = nullptr); - virtual ~CColorGradientCtrl(); - - //Key functions - int GetActiveKey() { return m_nActiveKey; }; - void SetActiveKey(int nIndex); - int InsertKey(QPoint point); - - // Turns on/off zooming and scroll support. - void SetNoZoom([[maybe_unused]] bool bNoZoom) { m_bNoZoom = false; }; - - void SetTimeRange(float tmin, float tmax) { m_fMinTime = tmin; m_fMaxTime = tmax; } - void SetValueRange(float tmin, float tmax) { m_fMinValue = tmin; m_fMaxValue = tmax; } - void SetTooltipValueScale(float x, float y) { m_fTooltipScaleX = x; m_fTooltipScaleY = y; }; - // Lock value of first and last key to be the same. - void LockFirstAndLastKeys(bool bLock) { m_bLockFirstLastKey = bLock; } - - void SetSpline(ISplineInterpolator* pSpline, bool bRedraw = false); - ISplineInterpolator* GetSpline(); - - void SetTimeMarker(float fTime); - - // Zoom in pixels per time unit. - void SetZoom(float fZoom); - void SetOrigin(float fOffset); - - typedef AZStd::function UpdateCallback; - void SetUpdateCallback(const UpdateCallback& cb) { m_updateCallback = cb; }; - - void SetNoTimeMarker(bool noTimeMarker); - -signals: - void change(); - void beforeChange(); - void activeKeyChange(); - -protected: - enum EHitCode - { - HIT_NOTHING, - HIT_KEY, - HIT_SPLINE, - }; - - void paintEvent(QPaintEvent* e) override; - void resizeEvent(QResizeEvent* event) override; - void mousePressEvent(QMouseEvent* event) override; - void mouseReleaseEvent(QMouseEvent* event) override; - void OnLButtonDown(QMouseEvent* event); - void mouseMoveEvent(QMouseEvent* event) override; - void OnLButtonUp(QMouseEvent* event); - void OnRButtonUp(QMouseEvent* event); - void mouseDoubleClickEvent(QMouseEvent* event) override; - void OnRButtonDown(QMouseEvent* event); - void keyPressEvent(QKeyEvent* event) override; - - // Drawing functions - void DrawGradient(QPaintEvent* e, QPainter* painter); - void DrawKeys(QPaintEvent* e, QPainter* painter); - void UpdateTooltip(QPoint pos); - - EHitCode HitTest(QPoint point); - - //Tracking support helper functions - void StartTracking(); - void TrackKey(QPoint point); - void StopTracking(QPoint point); - void RemoveKey(int nKey); - void EditKey(int nKey); - - QPoint KeyToPoint(int nKey); - QPoint TimeToPoint(float time); - void PointToTimeValue(QPoint point, float& time, ISplineInterpolator::ValueType& val); - float XOfsToTime(int x); - QPoint XOfsToPoint(int x); - - AZ::Color XOfsToColor(int x); - AZ::Color TimeToColor(float time); - - void ClearSelection(); - - void SendNotifyEvent(int nEvent); - - AZ::Color ValueToColor(ISplineInterpolator::ValueType val); - void ColorToValue(const AZ::Color& col, ISplineInterpolator::ValueType& val); - - -private: - void OnKeyColorChanged(const AZ::Color& color); - -private: - ISplineInterpolator* m_pSpline; - - bool m_bNoZoom; - - QRect m_rcClipRect; - QRect m_rcGradient; - QRect m_rcKeys; - - QPoint m_hitPoint; - EHitCode m_hitCode; - int m_nHitKeyIndex; - int m_nHitKeyDist; - QPoint m_curvePoint; - - float m_fTimeMarker; - - int m_nActiveKey; - int m_nKeyDrawRadius; - - bool m_bTracking; - - float m_fMinTime, m_fMaxTime; - float m_fMinValue, m_fMaxValue; - float m_fTooltipScaleX, m_fTooltipScaleY; - - bool m_bLockFirstLastKey; - - bool m_bNoTimeMarker; - - std::vector m_bSelectedKeys; - - UpdateCallback m_updateCallback; - - CWndGridHelper m_grid; -}; - -#endif // CRYINCLUDE_EDITOR_CONTROLS_COLORGRADIENTCTRL_H diff --git a/Code/Editor/Controls/ReflectedPropertyControl/PropertyCtrl.cpp b/Code/Editor/Controls/ReflectedPropertyControl/PropertyCtrl.cpp index c228fbda09..68c4acb95e 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/PropertyCtrl.cpp +++ b/Code/Editor/Controls/ReflectedPropertyControl/PropertyCtrl.cpp @@ -27,7 +27,6 @@ void RegisterReflectedVarHandlers() EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew LocalStringPropertyHandler()); EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew LightAnimationPropertyHandler()); EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew UserPopupWidgetHandler()); - EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew ColorCurveHandler()); EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew FloatCurveHandler()); EBUS_EVENT(AzToolsFramework::PropertyTypeRegistrationMessages::Bus, RegisterPropertyType, aznew MotionPropertyWidgetHandler()); } diff --git a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.cpp b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.cpp index b9f7e12e95..2278f55d95 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.cpp +++ b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.cpp @@ -170,30 +170,3 @@ bool FloatCurveHandler::ReadValuesIntoGUI([[maybe_unused]] size_t index, CSpline GUI->SetSpline(reinterpret_cast(instance.m_spline)); return false; } - - -QWidget* ColorCurveHandler::CreateGUI(QWidget *pParent) -{ - CColorGradientCtrl* gradientCtrl = new CColorGradientCtrl(pParent); - //connect(gradientCtrl, &CColorGradientCtrl::change, [gradientCtrl]() - //{ - // EBUS_EVENT(AzToolsFramework::PropertyEditorGUIMessages::Bus, RequestWrite, gradientCtrl); - //}); - gradientCtrl->SetTimeRange(0, 1); - gradientCtrl->setFixedHeight(36); - return gradientCtrl; - -} - -void ColorCurveHandler::ConsumeAttribute(CColorGradientCtrl*, AZ::u32, AzToolsFramework::PropertyAttributeReader*, const char*) -{} - -void ColorCurveHandler::WriteGUIValuesIntoProperty([[maybe_unused]] size_t index, [[maybe_unused]] CColorGradientCtrl* GUI, [[maybe_unused]] property_t& instance, [[maybe_unused]] AzToolsFramework::InstanceDataNode* node) -{} - -bool ColorCurveHandler::ReadValuesIntoGUI([[maybe_unused]] size_t index, CColorGradientCtrl* GUI, const property_t& instance, [[maybe_unused]] AzToolsFramework::InstanceDataNode* node) -{ - GUI->SetSpline(reinterpret_cast(instance.m_spline)); - return false; -} - diff --git a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h index e63a974c91..5ec24b679d 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h +++ b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h @@ -16,7 +16,6 @@ #include #include "ReflectedVar.h" #include "Util/VariablePropertyType.h" -#include "Controls/ColorGradientCtrl.h" #include "Controls/SplineCtrl.h" #include #endif @@ -82,17 +81,4 @@ public: void OnSplineChange(CSplineCtrl*); }; -class ColorCurveHandler : public QObject, public AzToolsFramework::PropertyHandler < CReflectedVarSpline, CColorGradientCtrl> -{ -public: - AZ_CLASS_ALLOCATOR(ColorCurveHandler, AZ::SystemAllocator, 0); - bool IsDefaultHandler() const override { return false; } - QWidget* CreateGUI(QWidget *pParent) override; - - AZ::u32 GetHandlerName(void) const override { return AZ_CRC("ePropertyColorCurve", 0xa30da4ec); } - - void ConsumeAttribute(CColorGradientCtrl* GUI, AZ::u32 attrib, AzToolsFramework::PropertyAttributeReader* attrValue, const char* debugName) override; - void WriteGUIValuesIntoProperty(size_t index, CColorGradientCtrl* GUI, property_t& instance, AzToolsFramework::InstanceDataNode* node) override; - bool ReadValuesIntoGUI(size_t index, CColorGradientCtrl* GUI, const property_t& instance, AzToolsFramework::InstanceDataNode* node) override; -}; #endif // CRYINCLUDE_EDITOR_UTILS_PROPERTYMISCCTRL_H diff --git a/Code/Editor/editor_lib_files.cmake b/Code/Editor/editor_lib_files.cmake index 031ad26d76..47b69765ba 100644 --- a/Code/Editor/editor_lib_files.cmake +++ b/Code/Editor/editor_lib_files.cmake @@ -330,8 +330,6 @@ set(FILES Commands/CommandManager.h Controls/BitmapToolTip.cpp Controls/BitmapToolTip.h - Controls/ColorGradientCtrl.cpp - Controls/ColorGradientCtrl.h Controls/ConsoleSCB.cpp Controls/ConsoleSCB.h Controls/ConsoleSCB.ui From bc9f6be4cf368e9ceb9672fafce9788df9a1b8ad Mon Sep 17 00:00:00 2001 From: AMZN-Phil Date: Thu, 21 Oct 2021 10:11:18 -0700 Subject: [PATCH 03/10] Change repo_uri passed into register repo to always be a str Signed-off-by: AMZN-Phil --- scripts/o3de/o3de/register.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/o3de/o3de/register.py b/scripts/o3de/o3de/register.py index 3c4d69a3c0..0de161177c 100644 --- a/scripts/o3de/o3de/register.py +++ b/scripts/o3de/o3de/register.py @@ -467,7 +467,7 @@ def register_restricted_path(json_data: dict, def register_repo(json_data: dict, - repo_uri: str or pathlib.Path, + repo_uri: str, remove: bool = False) -> int: if not repo_uri: logger.error(f'Repo URI cannot be empty.') @@ -566,7 +566,7 @@ def register(engine_path: pathlib.Path = None, external_subdir_path: pathlib.Path = None, template_path: pathlib.Path = None, restricted_path: pathlib.Path = None, - repo_uri: str or pathlib.Path = None, + repo_uri: str = None, default_engines_folder: pathlib.Path = None, default_projects_folder: pathlib.Path = None, default_gems_folder: pathlib.Path = None, @@ -641,7 +641,7 @@ def register(engine_path: pathlib.Path = None, return 1 result = result or register_restricted_path(json_data, restricted_path, remove, project_path, engine_path) - if isinstance(repo_uri, str) or isinstance(repo_uri, pathlib.PurePath): + if isinstance(repo_uri, str): if not repo_uri: logger.error(f'Repo URI cannot be empty.') return 1 From 64bbc700fa6be64c6ad572c8b8573e4d882ca69f Mon Sep 17 00:00:00 2001 From: Adi Bar-Lev <82479970+Adi-Amazon@users.noreply.github.com> Date: Thu, 21 Oct 2021 14:29:53 -0400 Subject: [PATCH 04/10] Hair - ShortCut rendering technique and pipeline with Marschner lighting model (#4871) - This rendering technique reduces the memory required per pipeline by 750MB compared to the PPLL technique!! - Using 3 screen buffers representing the closest 3 hair gragments depths. - Going through second geometry pass that disqualify any fragment further than the third depth, the technique blends the three closes shaders hair fragments. - Supports the Marschner lighting model (big change from the original TressFX 4.1 implementation) - The current technique is almost twice the performance of the PPLL but not quite as advacned when come to final visual quality. Remarks: Unlike the PPLL, this technique is still lacking some of the advanced features added to the PPLL such as 1. Back lobe (TT) conseal by depth comparison 2. Thickness dependency in light transfer (mainly TT) 3. Allowing TT transfer for thin separated hair strands (might be supported by default with no distinction) Signed-off-by: Adi-Amazon Signed-off-by: Adi-Amazon Signed-off-by: Adi-Amazon Signed-off-by: Adi-Amazon Co-authored-by: Adi-Amazon --- .../Passes/AtomTressFX_MainPipeline.pass | 6 +- .../Passes/AtomTressFX_PassTemplates.azasset | 23 ++ .../Assets/Passes/HairParentShortCutPass.pass | 391 ++++++++++++++++++ .../HairShortCutGeometryDepthAlpha.pass | 101 +++++ .../Passes/HairShortCutGeometryShading.pass | 155 +++++++ .../Passes/HairShortCutResolveColor.pass | 45 ++ .../Passes/HairShortCutResolveDepth.pass | 37 ++ .../{HairSrgs.azsli => HairComputeSrgs.azsli} | 2 +- .../Assets/Shaders/HairFullScreenUtils.azsli | 60 +++ .../Assets/Shaders/HairLighting.azsli | 17 +- .../Shaders/HairLightingEquations.azsli | 10 +- .../Assets/Shaders/HairRenderingFillPPLL.azsl | 42 +- .../Shaders/HairRenderingResolvePPLL.azsl | 23 +- .../HairShortCutGeometryDepthAlpha.azsl | 131 ++++++ .../HairShortCutGeometryDepthAlpha.shader | 45 ++ .../Shaders/HairShortCutGeometryShading.azsl | 176 ++++++++ .../HairShortCutGeometryShading.shader | 45 ++ .../Shaders/HairShortCutResolveColor.azsl | 63 +++ .../Shaders/HairShortCutResolveColor.shader | 41 ++ .../Shaders/HairShortCutResolveDepth.azsl | 65 +++ .../Shaders/HairShortCutResolveDepth.shader | 37 ++ .../Assets/Shaders/HairSimulationCompute.azsl | 2 +- ....azsli => HairSimulationComputeSrgs.azsli} | 4 +- .../Assets/Shaders/HairStrands.azsli | 37 +- .../Assets/Shaders/HairUtilities.azsli | 37 -- .../Code/Components/HairSystemComponent.cpp | 6 + .../Code/Passes/HairGeometryRasterPass.cpp | 13 +- .../Code/Passes/HairGeometryRasterPass.h | 3 +- .../Code/Passes/HairPPLLResolvePass.cpp | 33 +- .../Code/Passes/HairPPLLResolvePass.h | 13 +- .../Code/Passes/HairParentPass.cpp | 1 - .../HairShortCutGeometryDepthAlphaPass.cpp | 50 +++ .../HairShortCutGeometryDepthAlphaPass.h | 49 +++ .../HairShortCutGeometryShadingPass.cpp | 111 +++++ .../Passes/HairShortCutGeometryShadingPass.h | 69 ++++ .../Code/Rendering/HairFeatureProcessor.cpp | 171 ++++++-- .../Code/Rendering/HairFeatureProcessor.h | 28 +- .../Code/Rendering/HairRenderObject.cpp | 51 ++- .../Code/Rendering/HairRenderObject.h | 31 +- Gems/AtomTressFX/Hair_files.cmake | 56 ++- 40 files changed, 2055 insertions(+), 225 deletions(-) create mode 100644 Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass create mode 100644 Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryDepthAlpha.pass create mode 100644 Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryShading.pass create mode 100644 Gems/AtomTressFX/Assets/Passes/HairShortCutResolveColor.pass create mode 100644 Gems/AtomTressFX/Assets/Passes/HairShortCutResolveDepth.pass rename Gems/AtomTressFX/Assets/Shaders/{HairSrgs.azsli => HairComputeSrgs.azsli} (99%) create mode 100644 Gems/AtomTressFX/Assets/Shaders/HairFullScreenUtils.azsli create mode 100644 Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl create mode 100644 Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.shader create mode 100644 Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.azsl create mode 100644 Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.shader create mode 100644 Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.azsl create mode 100644 Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.shader create mode 100644 Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.azsl create mode 100644 Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.shader rename Gems/AtomTressFX/Assets/Shaders/{HairSimulationSrgs.azsli => HairSimulationComputeSrgs.azsli} (99%) create mode 100644 Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp create mode 100644 Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.h create mode 100644 Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.cpp create mode 100644 Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.h diff --git a/Gems/AtomTressFX/Assets/Passes/AtomTressFX_MainPipeline.pass b/Gems/AtomTressFX/Assets/Passes/AtomTressFX_MainPipeline.pass index 84bb85c3e1..9bfa746bf1 100644 --- a/Gems/AtomTressFX/Assets/Passes/AtomTressFX_MainPipeline.pass +++ b/Gems/AtomTressFX/Assets/Passes/AtomTressFX_MainPipeline.pass @@ -212,7 +212,11 @@ // instead of regular Depth as DepthStencil. Specifically, HairResolvePPLL.pass and the associated // .azsl file will need to be updated. "Name": "HairParentPass", - "TemplateName": "HairParentPassTemplate", + // Note: The following two lines represent the choice of rendering pipeline for the hair. + // You can either choose to use PPLL or ShortCut and accordingly change the flag + // 'm_usePPLLRenderTechnique' in the class 'HairFeatureProcessor.cpp' +// "TemplateName": "HairParentPassTemplate", + "TemplateName": "HairParentShortCutPassTemplate", "Enabled": true, "Connections": [ // Critical to keep DepthLinear as input - used to set the size of the Head PPLL image buffer. diff --git a/Gems/AtomTressFX/Assets/Passes/AtomTressFX_PassTemplates.azasset b/Gems/AtomTressFX/Assets/Passes/AtomTressFX_PassTemplates.azasset index a287664286..1580a4e1a9 100644 --- a/Gems/AtomTressFX/Assets/Passes/AtomTressFX_PassTemplates.azasset +++ b/Gems/AtomTressFX/Assets/Passes/AtomTressFX_PassTemplates.azasset @@ -8,6 +8,11 @@ "Name": "HairParentPassTemplate", "Path": "Passes/HairParentPass.pass" }, + { + "Name": "HairParentShortCutPassTemplate", + "Path": "Passes/HairParentShortCutPass.pass" + }, + { "Name": "HairGlobalShapeConstraintsComputePassTemplate", "Path": "Passes/HairGlobalShapeConstraintsCompute.pass" @@ -32,6 +37,7 @@ "Name": "HairUpdateFollowHairComputePassTemplate", "Path": "Passes/HairUpdateFollowHairCompute.pass" }, + { "Name": "HairPPLLRasterPassTemplate", "Path": "Passes/HairFillPPLL.pass" @@ -39,6 +45,23 @@ { "Name": "HairPPLLResolvePassTemplate", "Path": "Passes/HairResolvePPLL.pass" + }, + + { + "Name": "HairShortCutGeometryDepthAlphaPassTemplate", + "Path": "Passes/HairShortCutGeometryDepthAlpha.pass" + }, + { + "Name": "HairShortCutResolveDepthPassTemplate", + "Path": "Passes/HairShortCutResolveDepth.pass" + }, + { + "Name": "HairShortCutGeometryShadingPassTemplate", + "Path": "Passes/HairShortCutGeometryShading.pass" + }, + { + "Name": "HairShortCutResolveColorPassTemplate", + "Path": "Passes/HairShortCutResolveColor.pass" } ] } diff --git a/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass b/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass new file mode 100644 index 0000000000..7322611e2a --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass @@ -0,0 +1,391 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairParentShortCutPassTemplate", + "PassClass": "ParentPass", + "Slots": [ + { + "Name": "RenderTargetInputOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "RenderTarget" + }, + { // used for copy from MSAA to regular RT + "Name": "RenderTargetInputOnly", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + // This is the depth stencil buffer that is to be used by the fill pass + // to early reject pixels by depth and in the resolve pass to write the + // the hair depth + { + "Name": "Depth", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "DepthStencil" + }, + // Keep DepthLinear as input - used to set the size of the Head PPLL image buffer. + // If DepthLinear is not availbale - connect to another viewport (non MSAA) image. + { + "Name": "DepthLinearInput", + "SlotType": "InputOutput" + }, + { + "Name": "DepthLinear", + "SlotType": "Output" + }, + + // Lights & Shadows resources + { + "Name": "DirectionalShadowmap", + "SlotType": "Input" + }, + { + "Name": "DirectionalESM", + "SlotType": "Input" + }, + { + "Name": "ProjectedShadowmap", + "SlotType": "Input" + }, + { + "Name": "ProjectedESM", + "SlotType": "Input" + }, + { + "Name": "TileLightData", + "SlotType": "Input" + }, + { + "Name": "LightListRemapped", + "SlotType": "Input" + } + ], + "Connections": [ + { + "LocalSlot": "DepthLinear", + "AttachmentRef": { + "Pass": "DepthToDepthLinearPass", + "Attachment": "Output" + } + } + ], + "PassRequests": [ + { + "Name": "HairGlobalShapeConstraintsComputePass", + "TemplateName": "HairGlobalShapeConstraintsComputePassTemplate", + "Enabled": true + }, + { + "Name": "HairCalculateStrandLevelDataComputePass", + "TemplateName": "HairCalculateStrandLevelDataComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairGlobalShapeConstraintsComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + { + "Name": "HairVelocityShockPropagationComputePass", + "TemplateName": "HairVelocityShockPropagationComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairCalculateStrandLevelDataComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + { + "Name": "HairLocalShapeConstraintsComputePass", + "TemplateName": "HairLocalShapeConstraintsComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairVelocityShockPropagationComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + { + "Name": "HairLengthConstraintsWindAndCollisionComputePass", + "TemplateName": "HairLengthConstraintsWindAndCollisionComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairLocalShapeConstraintsComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + { + "Name": "HairUpdateFollowHairComputePass", + "TemplateName": "HairUpdateFollowHairComputePassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairLengthConstraintsWindAndCollisionComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + } + ] + }, + + // Render Target Copy from MS to Regular + { + "Name": "RenderTargetCopyPass", + "TemplateName": "FullscreenCopyTemplate", + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "RenderTargetInputOnly" + } + }, + { + "LocalSlot": "Output", + "AttachmentRef": { + "Pass": "This", + "Attachment": "Output" + } + } + ], + "ImageAttachments": [ + { + "Name": "Output", + "SizeSource": { + "Source": { + "Pass": "This", + "Attachment": "Input" + } + }, + "FormatSource": { + "Pass": "This", + "Attachment": "Input" + }, + "GenerateFullMipChain": false + } + ] + }, + + // Rendering Passes + { + "Name": "HairShortCutGeometryDepthAlphaPass", + "TemplateName": "HairShortCutGeometryDepthAlphaPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairUpdateFollowHairComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + }, + { + "LocalSlot": "Depth", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "Depth" + } + }, + { + "LocalSlot": "InverseAlphaRTOutput", + "AttachmentRef": { + "Pass": "This", + "Attachment": "InverseAlphaRTOutput" + } + }, + { + "LocalSlot": "HairDepthsTextureArray", + "AttachmentRef": { + "Pass": "This", + "Attachment": "HairDepthsTextureArray" + } + } + ] + }, + + { + "Name": "HairShortCutResolveDepthPass", + "TemplateName": "HairShortCutResolveDepthPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "Depth", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "Depth" + } + }, + { + "LocalSlot": "HairDepthsTextureArray", + "AttachmentRef": { + "Pass": "HairShortCutGeometryDepthAlphaPass", + "Attachment": "HairDepthsTextureArray" + } + } + ] + }, + + { + "Name": "HairShortCutGeometryShadingPass", + "TemplateName": "HairShortCutGeometryShadingPassTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "HairColorRenderTarget", + "AttachmentRef": { + "Pass": "This", + "Attachment": "HairColorRenderTarget" + } + }, + { // The final render target - this is MSAA mode RT - would it be cheaper to + // use non-MSAA and then copy? + "LocalSlot": "RenderTargetInputOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "RenderTargetInputOutput" + } + }, + { + "LocalSlot": "DepthLinear", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "DepthLinearInput" + } + }, + { + "LocalSlot": "Depth", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "Depth" + } + }, + { + "LocalSlot": "SkinnedHairSharedBuffer", + "AttachmentRef": { + "Pass": "HairUpdateFollowHairComputePass", + "Attachment": "SkinnedHairSharedBuffer" + } + }, + + // Shadows resources + { + "LocalSlot": "DirectionalShadowmap", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "DirectionalShadowmap" + } + }, + { + "LocalSlot": "DirectionalESM", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "DirectionalESM" + } + }, + { + "LocalSlot": "ProjectedShadowmap", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "ProjectedShadowmap" + } + }, + { + "LocalSlot": "ProjectedESM", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "ProjectedESM" + } + }, + + // Lights Resources + { + "LocalSlot": "TileLightData", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "TileLightData" + } + }, + { + "LocalSlot": "LightListRemapped", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "LightListRemapped" + } + } + ] + }, + + { + "Name": "HairShortCutResolveColorPass", + "TemplateName": "HairShortCutResolveColorPassTemplate", + "Enabled": true, + "Connections": [ + { // The final render target - this is MSAA mode RT - would it be cheaper to + // use non-MSAA and then copy? + "LocalSlot": "RenderTargetInputOutput", + "AttachmentRef": { + "Pass": "Parent", + "Attachment": "RenderTargetInputOutput" + } + }, + { + "LocalSlot": "AccumulatedInverseAlpha", + "AttachmentRef": { + "Pass": "HairShortCutGeometryDepthAlphaPass", + "Attachment": "InverseAlphaRTOutput" + } + }, + { + "LocalSlot": "HairColorTexture", + "AttachmentRef": { + "Pass": "HairShortCutGeometryShadingPass", + "Attachment": "HairColorRenderTarget" + } + } + ] + }, + + { + // This pass copies the updated depth buffer (now contains hair depth) to linear depth texture + // for downstream passes to use. This can be optimized even further by writing into the stencil + // buffer pixels that were touched by HairPPLLResolvePass hence preventing depth update unless + // it is hair. + "Name": "DepthToDepthLinearPass", + "TemplateName": "DepthToLinearTemplate", + "Enabled": true, + "Connections": [ + { + "LocalSlot": "Input", + "AttachmentRef": { + "Pass": "HairShortCutResolveDepthPass", + "Attachment": "Depth" + } + } + ] + } + + ] + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryDepthAlpha.pass b/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryDepthAlpha.pass new file mode 100644 index 0000000000..559224faa6 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryDepthAlpha.pass @@ -0,0 +1,101 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairShortCutGeometryDepthAlphaPassTemplate", + "PassClass": "HairShortCutGeometryDepthAlphaPass", + "Slots": [ + { + "Name": "SkinnedHairSharedBuffer", + "ShaderInputName": "m_skinnedHairSharedBuffer", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { // DepthStencil for early disqualifying the pixel based on depth. No write. + "Name": "Depth", + "SlotType": "Input", + "ScopeAttachmentUsage": "DepthStencil" + }, + { + // the regular render target is blended using inverse alpha to reduce the + // incoming color contribution based on the hair thickness and alpha. + "Name": "InverseAlphaRTOutput", + "SlotType": "Output", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "Clear", + "ClearValue": { + "Value": [ 1.0, 1.0, 1.0, 1.0 ] + }, + "StoreAction": "Store" + } + }, + { + "Name": "HairDepthsTextureArray", + "SlotType": "Output", + "ScopeAttachmentUsage": "Shader", + "ShaderInputName": "m_RWFragmentDepthsTexture", + "LoadStoreAction": { + "LoadAction": "Clear", + "ClearValue": { // reverse depth order: closer --> 1.0 + "Value": [ 0.0, 0.0, 0.0, 0.0 ] + }, + "StoreAction": "Store" + } + } + ], + "ImageAttachments": [ + { + // This buffer is used as the render target and should be at non-MSAA screen resolution + // to make sure no overwork is done. + "Name": "InverseAlphaRTOutput", + "SizeSource": { + "Source": { + "Pass": "Parent", + "Attachment": "DepthLinear" + } + }, + "ImageDescriptor": { + "Format": "R32_FLOAT", + "SharedQueueMask": "Graphics", + "BindFlags": [ + "Color", + "ShaderRead" + ] + } + }, + { + "Name": "HairDepthsTextureArray", + "SizeSource": { + "Source": { + "Pass": "Parent", + "Attachment": "DepthLinear" + } + }, + "ImageDescriptor": { + "Format": "R32_UINT", + "ArraySize": "3", + "SharedQueueMask": "Graphics", + "BindFlags": [ + "ShaderReadWrite", + "ShaderWrite", + "ShaderRead" + ] + } + } + ], + "PassData": { + "$type": "RasterPassData", + "DrawListTag": "HairGeometryDepthAlphaDrawList", + "PipelineViewTag": "MainCamera", + "PassSrgShaderAsset": { + // Looking for it in the Shaders directory relative to the Assets directory + "FilePath": "Shaders/HairShortCutGeometryDepthAlpha.shader" + } + } + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryShading.pass b/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryShading.pass new file mode 100644 index 0000000000..5940f8c549 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairShortCutGeometryShading.pass @@ -0,0 +1,155 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairShortCutGeometryShadingPassTemplate", + "PassClass": "HairShortCutGeometryShadingPass", + "Slots": [ + + { // Temporary color buffer to store the gathered shaded hair color - MSAA + "Name": "HairColorRenderTarget", + "SlotType": "Output", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "Clear", + "ClearValue": { // reverse depth order: closer --> 1.0 + "Value": [ 0.0, 0.0, 0.0, 0.0 ] + }, + "StoreAction": "Store" + } + }, + + { + // This RT is MSAA - is it cheaper to avoid doing this work and only do a copy at a separate pass? + "Name": "RenderTargetInputOutput", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { // Used to get the transform from screen space to world space. + "Name": "DepthLinear", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { // For comparing the depth to early disqualify but not to write + "Name": "Depth", + "SlotType": "Input", + "ScopeAttachmentUsage": "DepthStencil" + }, + { + "Name": "SkinnedHairSharedBuffer", + "ShaderInputName": "m_skinnedHairSharedBuffer", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + + //------------- Shadowing Resources ------------- + { + "Name": "DirectionalShadowmap", + "ShaderInputName": "m_directionalLightShadowmap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "DirectionalESM", + "ShaderInputName": "m_directionalLightExponentialShadowmap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "ProjectedShadowmap", + "ShaderInputName": "m_projectedShadowmaps", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + { + "Name": "ProjectedESM", + "ShaderInputName": "m_projectedExponentialShadowmap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ImageViewDesc": { + "IsArray": 1 + } + }, + + //------------- Lighting Resources ------------- + { + "Name": "BRDFTextureInput", + "ShaderInputName": "m_brdfMap", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader" + }, + { + "Name": "TileLightData", + "SlotType": "Input", + "ShaderInputName": "m_tileLightData", + "ScopeAttachmentUsage": "Shader" + }, + { + "Name": "LightListRemapped", + "SlotType": "Input", + "ShaderInputName": "m_lightListRemapped", + "ScopeAttachmentUsage": "Shader" + } + + ], + "ImageAttachments": [ + { + // The shader hair color render target - important to have at a non-MSAA mode + // so that no overwork is done on sampling. + "Name": "HairColorRenderTarget", + "SizeSource": { + "Source": { + "Pass": "Parent", + "Attachment": "RenderTargetInputOutput" + } + }, + "ImageDescriptor": { + "Format": "R16G16B16A16_FLOAT", + "SharedQueueMask": "Graphics", + "BindFlags": [ + "Color", + "ShaderRead" + ] + } + }, + { + "Name": "BRDFTexture", + "Lifetime": "Imported", + "AssetRef": { + "FilePath": "Textures/BRDFTexture.attimage" + } + } + ], + "Connections": [ + { + "LocalSlot": "BRDFTextureInput", + "AttachmentRef": { + "Pass": "This", + "Attachment": "BRDFTexture" + } + } + ], + "PassData": { + "$type": "RasterPassData", + "DrawListTag": "HairGeometryShadingDrawList", + "PipelineViewTag": "MainCamera", + "PassSrgShaderAsset": { + // Looking for it in the Shaders directory relative to the Assets directory + "FilePath": "Shaders/HairShortCutGeometryShading.shader" + } + } + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveColor.pass b/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveColor.pass new file mode 100644 index 0000000000..efbf993307 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveColor.pass @@ -0,0 +1,45 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairShortCutResolveColorPassTemplate", + "PassClass": "FullScreenTriangle", + "Slots": [ + { + // This RT is MSAA - is it cheaper to avoid doing this work and only do a copy at a separate pass? + "Name": "RenderTargetInputOutput", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "RenderTarget", + "LoadStoreAction": { + "LoadAction": "Load", + "StoreAction": "Store" + } + }, + { + "Name": "HairColorTexture", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderInputName": "m_hairColorTexture" + }, + { + "Name": "AccumulatedInverseAlpha", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderInputName": "m_accumInvAlpha" + } + ], + "Connections": [ + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + // Looking for it in the Shaders directory relative to the Assets directory + "FilePath": "Shaders/HairShortCutResolveColor.shader" + } + } + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveDepth.pass b/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveDepth.pass new file mode 100644 index 0000000000..021d711f1f --- /dev/null +++ b/Gems/AtomTressFX/Assets/Passes/HairShortCutResolveDepth.pass @@ -0,0 +1,37 @@ +{ + "Type": "JsonSerialization", + "Version": 1, + "ClassName": "PassAsset", + "ClassData": { + "PassTemplate": { + "Name": "HairShortCutResolveDepthPassTemplate", + "PassClass": "FullScreenTriangle", + "Slots": [ + //------ General Input/Output resources and Render Target ------ + { + "Name": "Depth", + "SlotType": "InputOutput", + "ScopeAttachmentUsage": "DepthStencil", + "LoadStoreAction": { + "LoadAction": "Load", + "StoreAction": "Store" + } + }, + { // This holds the K nearset depths. The furthest depth will be taken to be written in the depth buffer. + "Name": "HairDepthsTextureArray", + "SlotType": "Input", + "ScopeAttachmentUsage": "Shader", + "ShaderInputName": "m_fragmentDepthsTexture" + } + ], + "PassData": { + "$type": "FullscreenTrianglePassData", + "ShaderAsset": { + // Looking for it in the Shaders directory relative to the Assets directory + "FilePath": "Shaders/HairShortCutResolveDepth.shader" + } + } + } + } +} + diff --git a/Gems/AtomTressFX/Assets/Shaders/HairSrgs.azsli b/Gems/AtomTressFX/Assets/Shaders/HairComputeSrgs.azsli similarity index 99% rename from Gems/AtomTressFX/Assets/Shaders/HairSrgs.azsli rename to Gems/AtomTressFX/Assets/Shaders/HairComputeSrgs.azsli index cb0bdc2c6c..a4568588d5 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairSrgs.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairComputeSrgs.azsli @@ -29,7 +29,7 @@ // THE SOFTWARE. // //------------------------------------------------------------------------------ -// File: HairSRGs.azsli +// File: HairComputeSrgs.azsli // // Declarations of SRGs used by the hair shaders. //------------------------------------------------------------------------------ diff --git a/Gems/AtomTressFX/Assets/Shaders/HairFullScreenUtils.azsli b/Gems/AtomTressFX/Assets/Shaders/HairFullScreenUtils.azsli new file mode 100644 index 0000000000..7e57e8102c --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairFullScreenUtils.azsli @@ -0,0 +1,60 @@ +/* +* Modifications 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) AND MIT +* +*/ + +#include +#include +#include + +//============================================================================== +// Generate a fullscreen triangle from pipeline provided vertex id +VSOutput FullScreenVS(VSInput input) +{ + VSOutput OUT; + + float4 posTex = GetVertexPositionAndTexCoords(input.m_vertexID); + + OUT.m_texCoord = float2(posTex.z, posTex.w); // [To Do] - test sign of Y based on original code + OUT.m_position = float4(posTex.xy, 0.0, 1.0); + + return OUT; +} + +//============================================================================== +// Given the depth buffer depth of the current pixel and the fragment XY position, +// reconstruct the NDC. +// screenCoords - from 0.. dimension of the screen of the current pixel +// screenTexture - screen buffer texture representing the same resolution we work in +// sDepth - the depth buffer depth at the fragment location +// NDC - Normalized Device Coordinates = warped screen space ( -1.1, -1..1, 0..1 ) +float3 ScreenPosToNDC( Texture2D screenTexture, float2 screenCoords, float depth ) +{ + uint2 dimensions; + screenTexture.GetDimensions(dimensions.x, dimensions.y); + float2 UV = saturate(screenCoords / dimensions.xy); + + float x = UV.x * 2.0f - 1.0f; + float y = (1.0f - UV.y) * 2.0f - 1.0f; + float3 NDC = float3(x, y, depth); + + return NDC; +} + +// Given the depth buffer depth of the current pixel and the fragment XY position, +// reconstruct the world space position +float3 ScreenPosToWorldPos( + Texture2D screenTexture, float2 screenCoords, float depth, + inout float3 screenPosNDC ) +{ + screenPosNDC = ScreenPosToNDC(screenTexture, screenCoords, depth); + float4 projectedPos = float4(screenPosNDC, 1.0f); // warped projected space [0..1] + float4 positionVS = mul(ViewSrg::m_projectionMatrixInverse, projectedPos); + positionVS /= positionVS.w; // notice the normalization factor - crucial! + float4 positionWS = mul(ViewSrg::m_viewMatrixInverse, positionVS); + + return positionWS.xyz; +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairLighting.azsli b/Gems/AtomTressFX/Assets/Shaders/HairLighting.azsli index dcdcf43243..7beba248f7 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairLighting.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairLighting.azsli @@ -230,7 +230,7 @@ float3 CalculateLighting( return lightingData.diffuseLighting + lightingData.specularLighting; } -float3 TressFXShading(float2 pixelCoord, float depth, float3 vTangentCoverage, float3 baseColor, float thickness, int shaderParamIndex) +float3 TressFXShading(float2 pixelCoord, float depth, float3 tangent, float3 baseColor, float thickness, int shaderParamIndex) { float3 vNDC; // normalized device / screen coordinates: [-1..1, -1..1, 0..1] float3 vPositionWS = ScreenPosToWorldPos(PassSrg::m_linearDepth, pixelCoord, depth, vNDC); @@ -241,9 +241,6 @@ float3 TressFXShading(float2 pixelCoord, float depth, float3 vTangentCoverage, f float3 vViewDirWS = g_vEye - vPositionWS; - // Need to expand the tangent that was compressed to store in the buffer - float3 vTangent = normalize(vTangentCoverage.xyz * 2.f - 1.f); - //---- TressFX original lighting params setting ---- HairShadeParams params; params.m_color = baseColor; @@ -266,11 +263,19 @@ float3 TressFXShading(float2 pixelCoord, float depth, float3 vTangentCoverage, f if (o_hairLightingModel == HairLightingModel::Kajiya) { // This option should be removed and the Kajiya-Kay model should be operated from within // the Atom lighting loop. - accumulatedLight = SimplifiedHairLighting(vTangent, vPositionWS, vViewDirWS, params, vNDC); + accumulatedLight = SimplifiedHairLighting(tangent, vPositionWS, vViewDirWS, params, vNDC); } else { - accumulatedLight = CalculateLighting(screenCoords, vPositionWS, vViewDirWS, vTangent, thickness, params); + accumulatedLight = CalculateLighting(screenCoords, vPositionWS, vViewDirWS, tangent, thickness, params); } return accumulatedLight; } + +float3 TressFXShadingFullScreen(float2 pixelCoord, float depth, float3 compressedTangent, float3 baseColor, float thickness, int shaderParamIndex) +{ + // The tangent that was compressed to store in the PPLL structure + float3 tangent = normalize(compressedTangent.xyz * 2.f - 1.f); + + return TressFXShading(pixelCoord, depth, tangent, baseColor, thickness, shaderParamIndex); +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairLightingEquations.azsli b/Gems/AtomTressFX/Assets/Shaders/HairLightingEquations.azsli index 83ebc04521..4e4245f765 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairLightingEquations.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairLightingEquations.azsli @@ -66,7 +66,7 @@ option bool o_enableAzimuthCoeff = true; float M_R(Surface surface, float Lh, float sinLiPlusSinLr) { float a = 1.0f * surface.cuticleTilt; // Tilt is translate as the mean offset - float b = 0.5 * surface.roughnessA2; // Roughness is used as the standard deviation + float b = 0.5f * surface.roughnessA2; // Roughness is used as the standard deviation // return GaussianNormalized(sinLiPlusSinLr, a, b); // reference return GaussianNormalized(Lh, a, b); @@ -74,8 +74,8 @@ float M_R(Surface surface, float Lh, float sinLiPlusSinLr) float M_TT(Surface surface, float Lh, float sinLiPlusSinLr) { - float a = 1.0 * surface.cuticleTilt; - float b = 0.5 * surface.roughnessA2; + float a = 1.0f * surface.cuticleTilt; + float b = 0.5f * surface.roughnessA2; // return GaussianNormalized(sinLiPlusSinLr, a, b); // reference return GaussianNormalized(Lh, a, b); @@ -83,8 +83,8 @@ float M_TT(Surface surface, float Lh, float sinLiPlusSinLr) float M_TRT(Surface surface, float Lh, float sinLiPlusSinLr) { - float a = 1.5 * surface.cuticleTilt; - float b = 1.0 * surface.roughnessA2; + float a = 1.5f * surface.cuticleTilt; + float b = 1.0f * surface.roughnessA2; // return GaussianNormalized(sinLiPlusSinLr, a, b); // reference return GaussianNormalized(Lh, a, b); diff --git a/Gems/AtomTressFX/Assets/Shaders/HairRenderingFillPPLL.azsl b/Gems/AtomTressFX/Assets/Shaders/HairRenderingFillPPLL.azsl index 82c677faad..02777435f5 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairRenderingFillPPLL.azsl +++ b/Gems/AtomTressFX/Assets/Shaders/HairRenderingFillPPLL.azsl @@ -40,7 +40,8 @@ //! that can change between passes due to the application of skinning, simulation //! and physics affect and is then read by the rendering shaders. ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback -{ //! This shared buffer needs to match the SharedBuffer structure +{ + //! This shared buffer needs to match the SharedBuffer structure //! shared between all draw calls / dispatches for the hair skinning StructuredBuffer m_skinnedHairSharedBuffer; @@ -101,39 +102,8 @@ ShaderResourceGroup HairDynamicDataSrg : SRG_PerObject // space 1 - per instance #define g_GuideHairVertexTangents HairDynamicDataSrg::m_hairVertexTangents //============================================================================== -#include +#include // VS resides here //============================================================================== -//! Hair input structure to Pixel shaders -struct PS_INPUT_HAIR -{ - float4 Position : SV_POSITION; - float4 Tangent : Tangent; - float4 p0p1 : TEXCOORD0; - float4 StrandColor : TEXCOORD1; -}; - -//! Hair Render VS -PS_INPUT_HAIR RenderHairVS(uint vertexId : SV_VertexID) -{ -// uint2 scrSize; -// PassSrg::m_linearDepth.GetDimensions(scrSize.x, scrSize.y); -// TressFXVertex tressfxVert = GetExpandedTressFXVert(vertexId, g_vEye.xyz, float2(scrSize), g_mVP); - - // [To Do] Hair: the above code should replace the existing but requires modifications to - // the function GetExpandedTressFXVert. - // Note that in Atom g_vViewport is aspect ratio and NOT size. - TressFXVertex tressfxVert = GetExpandedTressFXVert(vertexId, g_vEye.xyz, g_vViewport.zw, g_mVP); - - - PS_INPUT_HAIR Output; - - Output.Position = tressfxVert.Position; - Output.Tangent = tressfxVert.Tangent; - Output.p0p1 = tressfxVert.p0p1; - Output.StrandColor = tressfxVert.StrandColor; - - return Output; -} // Allocate a new fragment location in fragment color, depth, and link buffers int AllocateFragment(int2 vScreenAddress) @@ -202,9 +172,9 @@ void PPLLFillPS(PS_INPUT_HAIR input) ////////////////////////////////////////////////////////////////////// // [To Do] Hair: anti aliasing via coverage requires work and is disabled for now - float3 vNDC = ScreenPosToNDC(PassSrg::m_linearDepth, input.Position.xy, input.Position.z); - uint2 dimensions; - PassSrg::m_linearDepth.GetDimensions(dimensions.x, dimensions.y); +// float3 vNDC = ScreenPosToNDC(PassSrg::m_linearDepth, input.Position.xy, input.Position.z); +// uint2 dimensions; +// PassSrg::m_linearDepth.GetDimensions(dimensions.x, dimensions.y); // float coverage = ComputeCoverage(input.p0p1.xy, input.p0p1.zw, vNDC.xy, float2(dimensions.x, dimensions.y)); float coverage = 1.0; ///////////////////////////////////////////////////////////////////// diff --git a/Gems/AtomTressFX/Assets/Shaders/HairRenderingResolvePPLL.azsl b/Gems/AtomTressFX/Assets/Shaders/HairRenderingResolvePPLL.azsl index 82c75280f6..3d72f1e21e 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairRenderingResolvePPLL.azsl +++ b/Gems/AtomTressFX/Assets/Shaders/HairRenderingResolvePPLL.azsl @@ -58,7 +58,7 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback // in the OIT process. // It can also be used to avoid the HW blend done at the end of the pixel // shader stage but HW blend might be cheaper than additional PS blend. - Texture2D m_frameBuffer; // The merged MSAA output + Texture2D m_frameBuffer; // The merged non-MSAA input // Linear depth is used for getting the screen to world transform Texture2D m_linearDepth; @@ -93,26 +93,11 @@ ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback #define HairParams PassSrg::m_hairParams //============================================================================== +#include // provides the Vertex Shader #include -#include -#include - -// Generates a fullscreen triangle from pipeline provided vertex id -VSOutput FullScreenVS(VSInput input) -{ - VSOutput OUT; - - float4 posTex = GetVertexPositionAndTexCoords(input.m_vertexID); - - OUT.m_texCoord = float2(posTex.z, posTex.w); - OUT.m_position = float4(posTex.x, posTex.y, 0.0, 1.0); - - return OUT; -} ////////////////////////////////////////////////////////////// // Bind data for PPLLResolvePS - #define NODE_DATA(x) LinkedListNodes[x].data #define NODE_NEXT(x) LinkedListNodes[x].uNext #define NODE_DEPTH(x) LinkedListNodes[x].depth @@ -298,7 +283,7 @@ float4 GatherLinkedList(float2 vfScreenAddress, float2 screenUV, inout float out uint shadeParamIndex; // So we know what settings to shade with float3 vColor = UnpackUintIntoFloat3Byte(color, shadeParamIndex); - float3 fragmentColor = TressFXShading(vfScreenAddress, fDepth, vTangent, vColor, fcolor.w, shadeParamIndex); + float3 fragmentColor = TressFXShadingFullScreen(vfScreenAddress, fDepth, vTangent, vColor, fcolor.w, shadeParamIndex); // Blend in the fragment color fcolor.xyz = fcolor.xyz * (1.f - alpha) + fragmentColor * alpha; @@ -355,7 +340,7 @@ float4 GetClosestFragment(float2 vfScreenAddress, float2 screenUV, inout float c float alpha = 1.0; uint shadeParamIndex; // the material index float3 vColor = UnpackUintIntoFloat3Byte(curColor, shadeParamIndex); - float3 fragmentColor = TressFXShading(vfScreenAddress, curDepth, vTangent, vColor, fcolor.w, shadeParamIndex); + float3 fragmentColor = TressFXShadingFullScreen(vfScreenAddress, curDepth, vTangent, vColor, fcolor.w, shadeParamIndex); // Blend in the fragment color fcolor.xyz = fcolor.xyz * (1.f - alpha) + (fragmentColor * alpha); diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl new file mode 100644 index 0000000000..23c926406f --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl @@ -0,0 +1,131 @@ +/* +* Modifications 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) AND MIT +* +*/ + +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include +#include + +//!------------------------------ SRG Structure -------------------------------- +//! Per pass SRG that holds the dynamic shared read-write buffer shared +//! across all dispatches and draw calls. It is used for all the dynamic buffers +//! that can change between passes due to the application of skinning, simulation +//! and physics affect. +//! Once the compute pases are done, it is read by the rendering shaders. +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + //! This shared buffer needs to match the SharedBuffer structure + //! shared between all draw calls / dispatches for the hair skinning + StructuredBuffer m_skinnedHairSharedBuffer; + + //! Based on [[vk::binding(0, 3)]] RWTexture2DArray RWFragmentDepthsTexture : register(u0, space3); + RWTexture2DArray m_RWFragmentDepthsTexture; +} +//============================================================================== + +//!------------------------------ SRG Structure -------------------------------- +//! Per instance/draw SRG representing dynamic read-write set of buffers +//! that are unique per instance and are shared and changed between passes due +//! to the application of skinning, simulation and physics affect. +//! It is then also read by the rendering shaders. +//! This Srg is NOT shared by the passes since it requires having barriers between +//! both passes and draw calls, instead, all buffers are allocated from a single +//! shared buffer (through BufferViews) and that buffer is then shared between +//! the passes via the PerPass Srg frequency. +ShaderResourceGroup HairDynamicDataSrg : SRG_PerObject // space 1 - per instance / object +{ + Buffer m_hairVertexPositions; + Buffer m_hairVertexTangents; + + //! Per hair object offset to the start location of each buffer within + //! 'm_skinnedHairSharedBuffer'. The offset is in bytes! + uint m_positionBufferOffset; + uint m_tangentBufferOffset; +}; +//------------------------------------------------------------------------------ +// Allow for the code to run with minimal changes - skinning / simulation compute passes +// Usage of per-instance buffer +#define g_GuideHairVertexPositions HairDynamicDataSrg::m_hairVertexPositions +#define g_GuideHairVertexTangents HairDynamicDataSrg::m_hairVertexTangents +//------------------------------------------------------------------------------ + +#include // VS resides here + +//!============================================================================= +//! Geometry Depth Alpha - First Pass of ShortCut Render +//! It is a Geometry pass that stores the K=3 front fragment depths, and accumulates +//! product of 1-alpha multiplications (fade out) of the input render target. +//! +//! Short explanation: in the original AMD implementation 1-alpha is multiplied +//! repeatedly with the incoming render target (back buffer) hence blending out +//! the existing back buffer color based on the density and transparency of the hair. +//! This implies that later on the hair color should be added based on the inverse +//! of this operation. +//!============================================================================= +[earlydepthstencil] +float HairShortCutDepthsAlphaPS(PS_INPUT_HAIR input) : SV_Target +{ + ////////////////////////////////////////////////////////////////////// + // [To Do] Hair: anti aliasing via coverage requires work and is disabled for now +// float3 vNDC = ScreenPosToNDC(PassSrg::m_linearDepth, input.Position.xy, input.Position.z); +// uint2 dimensions; +// PassSrg::m_linearDepth.GetDimensions(dimensions.x, dimensions.y); +// float coverage = ComputeCoverage(input.p0p1.xy, input.p0p1.zw, vNDC.xy, float2(dimensions.x, dimensions.y)); + float coverage = 1.0; + ///////////////////////////////////////////////////////////////////// + + float alpha = coverage * MatBaseColor.a; + + if (alpha < SHORTCUT_MIN_ALPHA) + return 1.0; + + int2 vScreenAddress = int2(input.Position.xy); + uint uDepth = asuint(input.Position.z); + uint uDepth0Prev, uDepth1Prev, uDepth2Prev; + + // Min of depth 0 and input depth - in Atom the Z order is reverse + // Original value is uDepth0Prev + InterlockedMax(PassSrg::m_RWFragmentDepthsTexture[uint3(vScreenAddress, 0)], uDepth, uDepth0Prev); + + // Min of depth 1 and greater of the last compare - in Atom the Z order is reverse + // If fragment opaque, always use input depth (don't need greater depths) + uDepth = (alpha > 0.98) ? uDepth : max(uDepth, uDepth0Prev); + + InterlockedMax(PassSrg::m_RWFragmentDepthsTexture[uint3(vScreenAddress, 1)], uDepth, uDepth1Prev); + + // Min of depth 2 and greater of the last compare - in Atom the Z order is reverse + // If fragment opaque, always use input depth (don't need greater depths) + uDepth = (alpha > 0.98) ? uDepth : max(uDepth, uDepth1Prev); + + InterlockedMax(PassSrg::m_RWFragmentDepthsTexture[uint3(vScreenAddress, 2)], uDepth, uDepth2Prev); + + // Accumulate the alpha multiplication from all hair components by multiplying the inverse and + // therefore going down towards 0. At the end product, the inverse will be taken as the hair + // alpha and the remainder will be used to blend the back buffer. + return 1.0 - alpha; +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.shader b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.shader new file mode 100644 index 0000000000..7cd1f44510 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryDepthAlpha.shader @@ -0,0 +1,45 @@ +{ + "Source" : "HairShortCutGeometryDepthAlpha.azsl", + "DrawList" : "HairGeometryDepthAlphaDrawList", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : true, + "WriteMask" : "Zero", // Avoid writing the depth + "CompareFunc" : "GreaterEqual" + // Originally in TressFX this is LessEqual - Atom is using reverse sort + }, + "Stencil" : + { + "Enable" : false + } + }, + + "BlendState" : + { + "Enable" : true, + "BlendSource" : "Zero", + "BlendDest" : "ColorSource", + "BlendOp" : "Add", + "BlendAlphaSource" : "Zero", + "BlendAlphaDest" : "AlphaSource", + "BlendAlphaOp" : "Add" + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "RenderHairVS", + "type": "Vertex" + }, + { + "name": "HairShortCutDepthsAlphaPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.azsl b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.azsl new file mode 100644 index 0000000000..c2a2958dfe --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.azsl @@ -0,0 +1,176 @@ +/* +* Modifications 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) AND MIT +* +*/ + +//------------------------------------------------------------------------------ +// Shader code related to lighting and shadowing for TressFX +//------------------------------------------------------------------------------ +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include +#include + +#define AMD_TRESSFX_MAX_HAIR_GROUP_RENDER 16 + +//!------------------------------ SRG Structure -------------------------------- +//! Per pass SRG that holds the dynamic shared read-write buffer shared +//! across all dispatches and draw calls. It is used for all the dynamic buffers +//! that can change between passes due to the application of skinning, simulation +//! and physics affect. +//! Once the compute pases are done, it is read by the rendering shaders. +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + //! This shared buffer needs to match the SharedBuffer structure + //! shared between all draw calls / dispatches for the hair skinning + StructuredBuffer m_skinnedHairSharedBuffer; + + //! Per hair object material array used by the PPLL resolve pass + //! Originally in TressFXRendering.hlsl this is space 0 + HairObjectShadeParams m_hairParams[AMD_TRESSFX_MAX_HAIR_GROUP_RENDER]; + + // Linear depth is used for getting the screen to world transform + Texture2D m_linearDepth; + + //------------------------------ + // Lighting Data + //------------------------------ + Sampler LinearSampler + { // Required by LightingData.azsli + MinFilter = Linear; + MagFilter = Linear; + MipFilter = Linear; + AddressU = Clamp; + AddressV = Clamp; + AddressW = Clamp; + }; + + Texture2DArray m_directionalLightShadowmap; + Texture2DArray m_directionalLightExponentialShadowmap; + Texture2DArray m_projectedShadowmaps; + Texture2DArray m_projectedExponentialShadowmap; + Texture2D m_brdfMap; + Texture2D m_tileLightData; + StructuredBuffer m_lightListRemapped; +} + +//------------------------------------------------------------------------------ +//! The hair objects' material array buffer used by the rendering resolve pass +#define HairParams PassSrg::m_hairParams +//============================================================================== + +//!------------------------------ SRG Structure -------------------------------- +//! Per instance/draw SRG representing dynamic read-write set of buffers +//! that are unique per instance and are shared and changed between passes due +//! to the application of skinning, simulation and physics affect. +//! It is then also read by the rendering shaders. +//! This Srg is NOT shared by the passes since it requires having barriers between +//! both passes and draw calls, instead, all buffers are allocated from a single +//! shared buffer (through BufferViews) and that buffer is then shared between +//! the passes via the PerPass Srg frequency. +ShaderResourceGroup HairDynamicDataSrg : SRG_PerObject // space 1 - per instance / object +{ + Buffer m_hairVertexPositions; + Buffer m_hairVertexTangents; + + //! Per hair object offset to the start location of each buffer within + //! 'm_skinnedHairSharedBuffer'. The offset is in bytes! + uint m_positionBufferOffset; + uint m_tangentBufferOffset; +}; +//------------------------------------------------------------------------------ +// Allow for the code to run with minimal changes - skinning / simulation compute passes +// Usage of per-instance buffer +#define g_GuideHairVertexPositions HairDynamicDataSrg::m_hairVertexPositions +#define g_GuideHairVertexTangents HairDynamicDataSrg::m_hairVertexTangents +//------------------------------------------------------------------------------ + +#include // VS resides here +#include // Required for world coordinates calculation +#include + +//!============================================================================= +//! Geometry Shading - Third Pass of ShortCut Render +//! Geometry pass that shades pixels that passes the early depth test. Due to this, it +//! is limited to the stored K near fragments due to previous depth write pass that +//! wrote the furthest depth of the K stored depths. +//! Colors are accumulated in the render target for a weighted average in final pass. +//! [To Do] - in the original short cut, the alpha is taken from the depth alpha pass +//!============================================================================= +[earlydepthstencil] +float4 HairShortCutGeometryColorPS(PS_INPUT_HAIR input) : SV_Target +{ + // Strand Color read in is either the BaseMatColor, or BaseMatColor modulated with a color read from texture + // on vertex shader for base color along with modulation by the tip color + float4 strandColor = float4(input.StrandColor.rgb, MatBaseColor.a); + + // If we are supporting strand UV texturing, further blend in the texture color/alpha + // Do this while computing NDC and coverage to hide latency from texture lookup + if (EnableStrandUV) + { + // Grab the uv in case we need it + float2 uv = float2(input.Tangent.w, input.StrandColor.w); + + // Apply StrandUVTiling + float2 strandUV = float2(uv.x, (uv.y * StrandUVTilingFactor) - floor(uv.y * StrandUVTilingFactor)); + + strandColor.rgb *= StrandAlbedoTexture.Sample(LinearWrapSampler, strandUV).rgb; + } + + ////////////////////////////////////////////////////////////////////// + // [To Do] Hair: anti aliasing via coverage requires work and is disabled for now +// float3 vNDC = ScreenPosToNDC(PassSrg::m_linearDepth, input.Position.xy, input.Position.z); +// uint2 dimensions; +// PassSrg::m_linearDepth.GetDimensions(dimensions.x, dimensions.y); +// float2 screenCoords = saturate(pixelCoord / dimensions.xy); +// float coverage = ComputeCoverage(input.p0p1.xy, input.p0p1.zw, vNDC.xy, float2(dimensions.x, dimensions.y)); +// original: float coverage = ComputeCoverage(input.p0p1.xy, input.p0p1.zw, vNDC.xy, g_vViewport.zw - g_vViewport.xy); + float coverage = 1.0; + ///////////////////////////////////////////////////////////////////// + + float alpha = coverage; + + // Update the alpha to have proper value (accounting for coverage, base alpha, and strand alpha) + alpha *= strandColor.w; + + // Early out + if (alpha < SHORTCUT_MIN_ALPHA) + { + return float4(0, 0, 0, 0); + } + + float2 pixelCoord = input.Position.xy; + float depth = input.Position.z; + // [To Do] - the thickness will need to be corrected somehow since this technique doesn't + // keeps track of the accumulated alpha / thickness + float thickness = alpha; + float3 shadedFragment = TressFXShading(pixelCoord, depth, input.Tangent.xyz, strandColor.rgb, thickness, RenderParamsIndex); + + // Color channel: Pre-multiply with alpha to create non-normalized weighted sum. + // Alpha Channel: Sum up all the hair alphas - this will be used to normalize the color + // per fragment at the next pass. + return float4(shadedFragment * alpha, alpha); +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.shader b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.shader new file mode 100644 index 0000000000..40e56006cd --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutGeometryShading.shader @@ -0,0 +1,45 @@ +{ + "Source" : "HairShortCutGeometryShading.azsl", + "DrawList" : "HairGeometryShadingDrawList", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : true, + "WriteMask" : "Zero", // Avoid writing the depth + "CompareFunc" : "GreaterEqual" + // Originally in TressFX this is LessEqual - Atom is using reverse sort + }, + "Stencil" : + { + "Enable" : false + } + }, + + "BlendState" : + { + "Enable" : true, + "BlendSource" : "One", + "BlendDest" : "One", + "BlendOp" : "Add", + "BlendAlphaSource" : "One", + "BlendAlphaDest" : "One", + "BlendAlphaOp" : "Add" + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "RenderHairVS", + "type": "Vertex" + }, + { + "name": "HairShortCutGeometryColorPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.azsl b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.azsl new file mode 100644 index 0000000000..f25c9dd711 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.azsl @@ -0,0 +1,63 @@ +/* +* Modifications 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) AND MIT +* +*/ + +#include +#include + +//!------------------------------ SRG Structure -------------------------------- +//! Per pass SRG that holds the dynamic shared read-write buffer shared +//! across all dispatches and draw calls. It is used for all the dynamic buffers +//! that can change between passes due to the application of skinning, simulation +//! and physics affect. +//! Once the compute pases are done, it is read by the rendering shaders. +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + // oiriginally: [[vk::binding(0, 0)]] Texture2D HaiColorTexture : register(t0, space0); + // oiriginally: [[vk::binding(1, 0)]] Texture2D AccumInvAlpha : register(t1, space0); + Texture2D m_hairColorTexture; + Texture2D m_accumInvAlpha; +} +//------------------------------------------------------------------------------ + +#include // provides the Vertex Shader + +//!============================================================================= +//! HairColorPS - Fourth Pass of ShortCut Render +//! Full-screen pass that finalizes the weighted average, and blends using the +//! accumulated 1-alpha product. +//!============================================================================= +[earlydepthstencil] +float4 HairShortCutResolveColorPS(VSOutput input) : SV_Target +{ + int2 vScreenAddress = int2(input.m_position.xy); + + float fInvAlpha = PassSrg::m_accumInvAlpha[vScreenAddress]; + float fAlpha = 1.0 - fInvAlpha; + + if (fAlpha < SHORTCUT_MIN_ALPHA) + { + // next we discard of non-hair pixels to avoid manipulating them depending + // on the alpha blend state - this is the safer and faster approach as there + // is no hair in these pixels + discard; + } + + float4 finalColor; + float weightSum = PassSrg::m_hairColorTexture[vScreenAddress].w; + + // Normalize the sum of the shaded fragment from the previous pass and + // then multiply it by the alpha blend of the hairs done in the depth-alpha pass. + finalColor.xyz = PassSrg::m_hairColorTexture[vScreenAddress] * fAlpha / weightSum; + + // The alpha is set to the inverse alpha of the hair so that the original + // background will be blended using this factor emulating single step alpha blend + // over the sum of all hair fragment blends. + finalColor.w = fInvAlpha; + + return finalColor; +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.shader b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.shader new file mode 100644 index 0000000000..6703c63f18 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveColor.shader @@ -0,0 +1,41 @@ +{ + "Source" : "HairShortCutResolveColor.azsl", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : false // Avoid comparing depth + }, + "Stencil" : + { + "Enable" : false + } + }, + + "BlendState" : + { + "Enable" : true, + "BlendSource" : "One", + "BlendDest" : "AlphaSource", + "BlendOp" : "Add", + "BlendAlphaSource" : "Zero", + "BlendAlphaDest" : "Zero", + "BlendAlphaOp" : "Add" + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "FullScreenVS", + "type": "Vertex" + }, + { + "name": "HairShortCutResolveColorPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.azsl b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.azsl new file mode 100644 index 0000000000..862b7c9015 --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.azsl @@ -0,0 +1,65 @@ +/* +* Modifications 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) AND MIT +* +*/ + +// +// Copyright (c) 2019 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#include + +//!------------------------------ SRG Structure -------------------------------- +//! Per pass SRG that holds the dynamic shared read-write buffer shared +//! across all dispatches and draw calls. It is used for all the dynamic buffers +//! that can change between passes due to the application of skinning, simulation +//! and physics affect. +//! Once the compute pases are done, it is read by the rendering shaders. +ShaderResourceGroup PassSrg : SRG_PerPass_WithFallback +{ + // Originally: [[vk::binding(0, 0)]] Texture2DArray FragmentDepthsTexture : register(t0, space0); + Texture2DArray m_fragmentDepthsTexture; +} +//------------------------------------------------------------------------------ + +#include // provides the Vertex Shader + +//!============================================================================= +//! Resolve Depth - Second Pass of ShortCut +//! Full-screen pass that writes the farthest of the stored K near depths so it +//! could be used for depth culling during the following geometry shading pass. +//!============================================================================= +float HairShortCutResolveDepthPS(VSOutput input) : SV_Depth +{ + // Blend the layers of fragments from back to front + int2 vScreenAddress = int2(input.m_position.xy); + + // Write farthest depth value for culling in the next pass. + // It may be the initial value of 1.0 if there were not enough fragments to write all depths, but then culling not important. + const int farthestDepthIndex = 2; + uint uDepth = PassSrg::m_fragmentDepthsTexture[uint3(vScreenAddress, farthestDepthIndex)]; + + // The following line is writing the depth into the actual depth buffer + return asfloat(uDepth); +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.shader b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.shader new file mode 100644 index 0000000000..5b518c3bfc --- /dev/null +++ b/Gems/AtomTressFX/Assets/Shaders/HairShortCutResolveDepth.shader @@ -0,0 +1,37 @@ +{ + "Source" : "HairShortCutResolveDepth.azsl", + + "DepthStencilState" : + { + "Depth" : + { + "Enable" : true, // test the written depth and accept/discard based on the depth buffer + "CompareFunc" : "GreaterEqual" + // Originally in TressFX this is LessEqual - Atom is using reverse sort + }, + "Stencil" : + { + "Enable" : false + } + }, + + "BlendState" : + { + "Enable" : false + }, + + "ProgramSettings": + { + "EntryPoints": + [ + { + "name": "FullScreenVS", + "type": "Vertex" + }, + { + "name": "HairShortCutResolveDepthPS", + "type": "Fragment" + } + ] + } +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairSimulationCompute.azsl b/Gems/AtomTressFX/Assets/Shaders/HairSimulationCompute.azsl index 496aa5c7da..ed6ec5de27 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairSimulationCompute.azsl +++ b/Gems/AtomTressFX/Assets/Shaders/HairSimulationCompute.azsl @@ -31,7 +31,7 @@ // THE SOFTWARE. // //-------------------------------------------------------------------------------------- -#include +#include #include //-------------------------------------------------------------------------------------- diff --git a/Gems/AtomTressFX/Assets/Shaders/HairSimulationSrgs.azsli b/Gems/AtomTressFX/Assets/Shaders/HairSimulationComputeSrgs.azsli similarity index 99% rename from Gems/AtomTressFX/Assets/Shaders/HairSimulationSrgs.azsli rename to Gems/AtomTressFX/Assets/Shaders/HairSimulationComputeSrgs.azsli index f95dac92cf..3ded3ab2af 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairSimulationSrgs.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairSimulationComputeSrgs.azsli @@ -29,13 +29,13 @@ // THE SOFTWARE. // //------------------------------------------------------------------------------ -// File: HairSRGs.azsli +// File: HairSimulationComputeSrgs.azsli // // Declarations of SRGs used by the hair shaders. //------------------------------------------------------------------------------ #pragma once -#include +#include //!----------------------------------------------------------------------------- //! diff --git a/Gems/AtomTressFX/Assets/Shaders/HairStrands.azsli b/Gems/AtomTressFX/Assets/Shaders/HairStrands.azsli index b8c4ea9eb4..abbbc8c0de 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairStrands.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairStrands.azsli @@ -66,12 +66,22 @@ float3 GetSharedTangent(int tangentIndex) ); } +//! Hair vertex geometry output - input structure for the Pixel shaders struct TressFXVertex { float4 Position; - float4 Tangent; + float4 Tangent; // xyz = Tangent, w = Strand U float4 p0p1; - float4 StrandColor; + float4 StrandColor; // xyz = Strand Color, w = Strand V +}; + +//! Matching structure to carry out as VS output / PS input +struct PS_INPUT_HAIR +{ + float4 Position : SV_POSITION; + float4 Tangent : Tangent; + float4 p0p1 : TEXCOORD0; + float4 StrandColor : TEXCOORD1; }; float3 GetStrandColor(int index, float fractionOfStrand) @@ -178,5 +188,26 @@ TressFXVertex GetExpandedTressFXShadowVert(uint vertexId, float3 eye, float2 win return Output; } -// EndHLSL +//!============================================================================= +//! Hair Render VS - Used by all geometry hair shaders +//!============================================================================= +PS_INPUT_HAIR RenderHairVS(uint vertexId : SV_VertexID) +{ + PS_INPUT_HAIR vsOutput; + // uint2 scrSize; + // PassSrg::m_linearDepth.GetDimensions(scrSize.x, scrSize.y); + // TressFXVertex tressfxVert = GetExpandedTressFXVert(vertexId, g_vEye.xyz, float2(scrSize), g_mVP); + + // [To Do] Hair: the above code should replace the existing but requires modifications to + // the function GetExpandedTressFXVert. + // Note that in Atom g_vViewport is aspect ratio and NOT size. + TressFXVertex tressfxVert = GetExpandedTressFXVert(vertexId, g_vEye.xyz, g_vViewport.zw, g_mVP); + + vsOutput.Position = tressfxVert.Position; + vsOutput.Tangent = tressfxVert.Tangent; + vsOutput.p0p1 = tressfxVert.p0p1; + vsOutput.StrandColor = tressfxVert.StrandColor; + + return vsOutput; +} diff --git a/Gems/AtomTressFX/Assets/Shaders/HairUtilities.azsli b/Gems/AtomTressFX/Assets/Shaders/HairUtilities.azsli index 1d50d3c040..f91ba1ff3b 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairUtilities.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairUtilities.azsli @@ -34,9 +34,6 @@ #pragma once -#include - - #define SHORTCUT_MIN_ALPHA 0.02 #define TRESSFX_FLOAT_EPSILON 1e-7 @@ -52,40 +49,6 @@ float4 MatrixMult(float4x4 m, float4 v) return mul(m, v); } -// Given the depth buffer depth of the current pixel and the fragment XY position, -// reconstruct the NDC. -// screenCoords - from 0.. dimension of the screen of the current pixel -// screenTexture - screen buffer texture representing the same resolution we work in -// sDepth - the depth buffer depth at the fragment location -// NDC - Normalized Device Coordinates = warped screen space ( -1.1, -1..1, 0..1 ) -float3 ScreenPosToNDC( Texture2D screenTexture, float2 screenCoords, float depth ) -{ - uint2 dimensions; - screenTexture.GetDimensions(dimensions.x, dimensions.y); - float2 UV = saturate(screenCoords / dimensions.xy); - - float x = UV.x * 2.0f - 1.0f; - float y = (1.0f - UV.y) * 2.0f - 1.0f; - float3 NDC = float3(x, y, depth); - - return NDC; -} - -// Given the depth buffer depth of the current pixel and the fragment XY position, -// reconstruct the world space position -float3 ScreenPosToWorldPos( - Texture2D screenTexture, float2 screenCoords, float depth, - inout float3 screenPosNDC ) -{ - screenPosNDC = ScreenPosToNDC(PassSrg::m_linearDepth, screenCoords, depth); - float4 projectedPos = float4(screenPosNDC, 1.0f); // warped projected space [0..1] - float4 positionVS = mul(ViewSrg::m_projectionMatrixInverse, projectedPos); - positionVS /= positionVS.w; // notice the normalization factor - crucial! - float4 positionWS = mul(ViewSrg::m_viewMatrixInverse, positionVS); - - return positionWS.xyz; -} - // Pack a float4 into an uint uint PackFloat4IntoUint(float4 vValue) { diff --git a/Gems/AtomTressFX/Code/Components/HairSystemComponent.cpp b/Gems/AtomTressFX/Code/Components/HairSystemComponent.cpp index 66ac3d2e09..2ae8b9d97d 100644 --- a/Gems/AtomTressFX/Code/Components/HairSystemComponent.cpp +++ b/Gems/AtomTressFX/Code/Components/HairSystemComponent.cpp @@ -80,8 +80,14 @@ namespace AZ // Load the AtomTressFX pass classes passSystem->AddPassCreator(Name("HairSkinningComputePass"), &HairSkinningComputePass::Create); + + // Load the PPLL render method passes passSystem->AddPassCreator(Name("HairPPLLRasterPass"), &HairPPLLRasterPass::Create); passSystem->AddPassCreator(Name("HairPPLLResolvePass"), &HairPPLLResolvePass::Create); + + // Load the ShortCut render method passes + passSystem->AddPassCreator(Name("HairShortCutGeometryDepthAlphaPass"), &HairShortCutGeometryDepthAlphaPass::Create); + passSystem->AddPassCreator(Name("HairShortCutGeometryShadingPass"), &HairShortCutGeometryShadingPass::Create); } void HairSystemComponent::Deactivate() diff --git a/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.cpp b/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.cpp index 7af5903cf3..b9c0d2e379 100644 --- a/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.cpp +++ b/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.cpp @@ -165,6 +165,15 @@ namespace AZ return true; } + Data::Instance HairGeometryRasterPass::GetShader() + { + if (!m_initialized || !m_shader) + { + AZ_Error("Hair Gem", LoadShaderAndPipelineState(), "HairGeometryRasterPass could not initialize pipeline or shader"); + } + return m_shader; + } + void HairGeometryRasterPass::SchedulePacketBuild(HairRenderObject* hairObject) { m_newRenderObjects.insert(hairObject); @@ -188,7 +197,7 @@ namespace AZ // The PerPass is gathered through the RasterPass::m_shaderResourceGroup AZStd::lock_guard lock(m_mutex); - return hairObject->BuildPPLLDrawPacket(drawRequest); + return hairObject->BuildDrawPacket(m_shader.get(), drawRequest); } bool HairGeometryRasterPass::AddDrawPackets(AZStd::list>& hairRenderObjects) @@ -205,7 +214,7 @@ namespace AZ for (auto& renderObject : hairRenderObjects) { - const RHI::DrawPacket* drawPacket = renderObject->GetFillDrawPacket(); + const RHI::DrawPacket* drawPacket = renderObject->GetGeometrylDrawPacket(m_shader.get()); if (!drawPacket) { // might not be an error - the object might have just been added and the DrawPacket is // scheduled to be built when the render frame begins diff --git a/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.h b/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.h index a226d2c294..d3ba22039d 100644 --- a/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.h +++ b/Gems/AtomTressFX/Code/Passes/HairGeometryRasterPass.h @@ -51,7 +51,7 @@ namespace AZ //! The following will be called when an object was added or shader has been compiled void SchedulePacketBuild(HairRenderObject* hairObject); - Data::Instance GetShader() { return m_shader; } + Data::Instance GetShader(); void SetFeatureProcessor(HairFeatureProcessor* featureProcessor) { @@ -76,7 +76,6 @@ namespace AZ // Pass behavior overrides void InitializeInternal() override; -// void BuildInternal() override; void FrameBeginInternal(FramePrepareParams params) override; // Scope producer functions... diff --git a/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.cpp b/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.cpp index fa643a5488..de66d91e0b 100644 --- a/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.cpp +++ b/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.cpp @@ -30,6 +30,17 @@ namespace AZ HairPPLLResolvePass::HairPPLLResolvePass(const RPI::PassDescriptor& descriptor) : RPI::FullscreenTrianglePass(descriptor) { + o_enableShadows = AZ::Name("o_enableShadows"); + o_enableDirectionalLights = AZ::Name("o_enableDirectionalLights"); + o_enablePunctualLights = AZ::Name("o_enablePunctualLights"); + o_enableAreaLights = AZ::Name("o_enableAreaLights"); + o_enableIBL = AZ::Name("o_enableIBL"); + o_hairLightingModel = AZ::Name("o_hairLightingModel"); + o_enableMarschner_R = AZ::Name("o_enableMarschner_R"); + o_enableMarschner_TRT = AZ::Name("o_enableMarschner_TRT"); + o_enableMarschner_TT = AZ::Name("o_enableMarschner_TT"); + o_enableLongtitudeCoeff = AZ::Name("o_enableLongtitudeCoeff"); + o_enableAzimuthCoeff = AZ::Name("o_enableAzimuthCoeff"); } void HairPPLLResolvePass::UpdateGlobalShaderOptions() @@ -38,17 +49,17 @@ namespace AZ m_featureProcessor->GetHairGlobalSettings(m_hairGlobalSettings); - shaderOption.SetValue(AZ::Name("o_enableShadows"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableShadows }); - shaderOption.SetValue(AZ::Name("o_enableDirectionalLights"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableDirectionalLights }); - shaderOption.SetValue(AZ::Name("o_enablePunctualLights"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enablePunctualLights }); - shaderOption.SetValue(AZ::Name("o_enableAreaLights"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAreaLights }); - shaderOption.SetValue(AZ::Name("o_enableIBL"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableIBL }); - shaderOption.SetValue(AZ::Name("o_hairLightingModel"), AZ::Name{ "HairLightingModel::" + AZStd::string(HairLightingModelNamespace::ToString(m_hairGlobalSettings.m_hairLightingModel)) }); - shaderOption.SetValue(AZ::Name("o_enableMarschner_R"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_R }); - shaderOption.SetValue(AZ::Name("o_enableMarschner_TRT"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TRT }); - shaderOption.SetValue(AZ::Name("o_enableMarschner_TT"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TT }); - shaderOption.SetValue(AZ::Name("o_enableLongtitudeCoeff"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableLongtitudeCoeff }); - shaderOption.SetValue(AZ::Name("o_enableAzimuthCoeff"), AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAzimuthCoeff }); + shaderOption.SetValue(o_enableShadows, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableShadows }); + shaderOption.SetValue(o_enableDirectionalLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableDirectionalLights }); + shaderOption.SetValue(o_enablePunctualLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enablePunctualLights }); + shaderOption.SetValue(o_enableAreaLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAreaLights }); + shaderOption.SetValue(o_enableIBL, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableIBL }); + shaderOption.SetValue(o_hairLightingModel, AZ::Name{ "HairLightingModel::" + AZStd::string(HairLightingModelNamespace::ToString(m_hairGlobalSettings.m_hairLightingModel)) }); + shaderOption.SetValue(o_enableMarschner_R, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_R }); + shaderOption.SetValue(o_enableMarschner_TRT, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TRT }); + shaderOption.SetValue(o_enableMarschner_TT, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TT }); + shaderOption.SetValue(o_enableLongtitudeCoeff, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableLongtitudeCoeff }); + shaderOption.SetValue(o_enableAzimuthCoeff, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAzimuthCoeff }); m_shaderOptions = shaderOption.GetShaderVariantKeyFallbackValue(); } diff --git a/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.h b/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.h index bf1a3f768e..036659b9a5 100644 --- a/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.h +++ b/Gems/AtomTressFX/Code/Passes/HairPPLLResolvePass.h @@ -58,9 +58,20 @@ namespace AZ void CompileResources(const RHI::FrameGraphCompileContext& context) override; private: + AZ::Name o_enableShadows; + AZ::Name o_enableDirectionalLights; + AZ::Name o_enablePunctualLights; + AZ::Name o_enableAreaLights; + AZ::Name o_enableIBL; + AZ::Name o_hairLightingModel; + AZ::Name o_enableMarschner_R; + AZ::Name o_enableMarschner_TRT; + AZ::Name o_enableMarschner_TT; + AZ::Name o_enableLongtitudeCoeff; + AZ::Name o_enableAzimuthCoeff; + HairPPLLResolvePass(const RPI::PassDescriptor& descriptor); - private: void UpdateGlobalShaderOptions(); HairGlobalSettings m_hairGlobalSettings; diff --git a/Gems/AtomTressFX/Code/Passes/HairParentPass.cpp b/Gems/AtomTressFX/Code/Passes/HairParentPass.cpp index 24cd6465dd..1b0b034dba 100644 --- a/Gems/AtomTressFX/Code/Passes/HairParentPass.cpp +++ b/Gems/AtomTressFX/Code/Passes/HairParentPass.cpp @@ -9,7 +9,6 @@ #include #include #include -#include namespace AZ { diff --git a/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp new file mode 100644 index 0000000000..5e402e33f6 --- /dev/null +++ b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp @@ -0,0 +1,50 @@ +/* + * 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 +#include +#include + +#include +#include +#include + +namespace AZ +{ + namespace Render + { + namespace Hair + { + + HairShortCutGeometryDepthAlphaPass::HairShortCutGeometryDepthAlphaPass(const RPI::PassDescriptor& descriptor) + : HairGeometryRasterPass(descriptor) + { + SetShaderPath("Shaders/hairshortcutgeometrydepthalpha.azshader"); + } + + RPI::Ptr HairShortCutGeometryDepthAlphaPass::Create(const RPI::PassDescriptor& descriptor) + { + RPI::Ptr pass = aznew HairShortCutGeometryDepthAlphaPass(descriptor); + return pass; + } + + void HairShortCutGeometryDepthAlphaPass::BuildInternal() + { + RasterPass::BuildInternal(); // change this to call parent if the method exists + + if (!AcquireFeatureProcessor()) + { + return; + } + + LoadShaderAndPipelineState(); + } + + } // namespace Hair + } // namespace Render +} // namespace AZ diff --git a/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.h b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.h new file mode 100644 index 0000000000..da6e28fe31 --- /dev/null +++ b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryDepthAlphaPass.h @@ -0,0 +1,49 @@ +/* + * 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 + +namespace AZ +{ + namespace RHI + { + struct DrawItem; + } + + namespace Render + { + namespace Hair + { + //! This geometry pass uses the following Srgs: + //! - PerPassSrg shared by all hair passes for the shared dynamic buffer + //! - PerMaterialSrg - used solely by this pass to alter the vertices and apply the visual + //! hair properties to each fragment. + //! - HairDynamicDataSrg (PerObjectSrg) - shared buffers views for this hair object only. + //! - PerViewSrg and PerSceneSrg - as per the data from Atom. + class HairShortCutGeometryDepthAlphaPass + : public HairGeometryRasterPass + { + AZ_RPI_PASS(HairShortCutGeometryDepthAlphaPass); + + public: + AZ_RTTI(HairShortCutGeometryDepthAlphaPass, "{F09A0411-B1FF-4085-98E7-6B8B0E1B2C3D}", HairGeometryRasterPass); + AZ_CLASS_ALLOCATOR(HairShortCutGeometryDepthAlphaPass, SystemAllocator, 0); + + static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); + + protected: + explicit HairShortCutGeometryDepthAlphaPass(const RPI::PassDescriptor& descriptor); + + // Pass behavior overrides + void BuildInternal() override; + }; + + } // namespace Hair + } // namespace Render +} // namespace AZ diff --git a/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.cpp b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.cpp new file mode 100644 index 0000000000..eda7f9f83a --- /dev/null +++ b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.cpp @@ -0,0 +1,111 @@ +/* + * 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 +#include +#include + +#include +#include +#include + +namespace AZ +{ + namespace Render + { + namespace Hair + { + + HairShortCutGeometryShadingPass::HairShortCutGeometryShadingPass(const RPI::PassDescriptor& descriptor) + : HairGeometryRasterPass(descriptor) + { + o_enableShadows = AZ::Name("o_enableShadows"); + o_enableDirectionalLights = AZ::Name("o_enableDirectionalLights"); + o_enablePunctualLights = AZ::Name("o_enablePunctualLights"); + o_enableAreaLights = AZ::Name("o_enableAreaLights"); + o_enableIBL = AZ::Name("o_enableIBL"); + o_hairLightingModel = AZ::Name("o_hairLightingModel"); + o_enableMarschner_R = AZ::Name("o_enableMarschner_R"); + o_enableMarschner_TRT = AZ::Name("o_enableMarschner_TRT"); + o_enableMarschner_TT = AZ::Name("o_enableMarschner_TT"); + o_enableLongtitudeCoeff = AZ::Name("o_enableLongtitudeCoeff"); + o_enableAzimuthCoeff = AZ::Name("o_enableAzimuthCoeff"); + + SetShaderPath("Shaders/hairshortcutgeometryshading.azshader"); + } + + RPI::Ptr HairShortCutGeometryShadingPass::Create(const RPI::PassDescriptor& descriptor) + { + RPI::Ptr pass = aznew HairShortCutGeometryShadingPass(descriptor); + return pass; + } + + void HairShortCutGeometryShadingPass::UpdateGlobalShaderOptions() + { + RPI::ShaderOptionGroup shaderOption = m_shader->CreateShaderOptionGroup(); + + m_featureProcessor->GetHairGlobalSettings(m_hairGlobalSettings); + + shaderOption.SetValue(o_enableShadows, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableShadows }); + shaderOption.SetValue(o_enableDirectionalLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableDirectionalLights }); + shaderOption.SetValue(o_enablePunctualLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enablePunctualLights }); + shaderOption.SetValue(o_enableAreaLights, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAreaLights }); + shaderOption.SetValue(o_enableIBL, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableIBL }); + shaderOption.SetValue(o_hairLightingModel, AZ::Name{ "HairLightingModel::" + AZStd::string(HairLightingModelNamespace::ToString(m_hairGlobalSettings.m_hairLightingModel)) }); + shaderOption.SetValue(o_enableMarschner_R, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_R }); + shaderOption.SetValue(o_enableMarschner_TRT, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TRT }); + shaderOption.SetValue(o_enableMarschner_TT, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableMarschner_TT }); + shaderOption.SetValue(o_enableLongtitudeCoeff, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableLongtitudeCoeff }); + shaderOption.SetValue(o_enableAzimuthCoeff, AZ::RPI::ShaderOptionValue{ m_hairGlobalSettings.m_enableAzimuthCoeff }); + + m_shaderOptions = shaderOption.GetShaderVariantKeyFallbackValue(); + } + + void HairShortCutGeometryShadingPass::CompileResources(const RHI::FrameGraphCompileContext& context) + { + if (!m_shaderResourceGroup || !AcquireFeatureProcessor()) + { + AZ_Error("Hair Gem", m_shaderResourceGroup, "HairShortCutGeometryShadingPass: missing Srg or no feature processor yet"); + return; // no error message due to FP - initialization not complete yet, wait for the next frame + } + + UpdateGlobalShaderOptions(); + + if (m_shaderResourceGroup->HasShaderVariantKeyFallbackEntry()) + { + m_shaderResourceGroup->SetShaderVariantKeyFallbackValue(m_shaderOptions); + } + + // Update the material array constant buffer within the per pass srg + SrgBufferDescriptor descriptor = SrgBufferDescriptor( + RPI::CommonBufferPoolType::Constant, RHI::Format::Unknown, + sizeof(AMD::TressFXShadeParams), 1, + Name{ "HairMaterialsArray" }, Name{ "m_hairParams" }, 0, 0 + ); + + m_featureProcessor->GetMaterialsArray().UpdateGPUData(m_shaderResourceGroup, descriptor); + + // Compilation of remaining srgs will be done by the parent class + RPI::RasterPass::CompileResources(context); + } + + void HairShortCutGeometryShadingPass::BuildInternal() + { + RasterPass::BuildInternal(); // change this to call parent if the method exists + + if (!AcquireFeatureProcessor()) + { + return; + } + + LoadShaderAndPipelineState(); + } + + } // namespace Hair + } // namespace Render +} // namespace AZ diff --git a/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.h b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.h new file mode 100644 index 0000000000..d728803473 --- /dev/null +++ b/Gems/AtomTressFX/Code/Passes/HairShortCutGeometryShadingPass.h @@ -0,0 +1,69 @@ +/* + * 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 +#include + +namespace AZ +{ + namespace RHI + { + struct DrawItem; + } + + namespace Render + { + namespace Hair + { + //! This geometry pass uses the following Srgs: + //! - PerPassSrg shared by all hair passes for the shared dynamic buffer + //! - PerMaterialSrg - used solely by this pass to alter the vertices and apply the visual + //! hair properties to each fragment. + //! - HairDynamicDataSrg (PerObjectSrg) - shared buffers views for this hair object only. + //! - PerViewSrg and PerSceneSrg - as per the data from Atom. + class HairShortCutGeometryShadingPass + : public HairGeometryRasterPass + { + AZ_RPI_PASS(HairShortCutGeometryShadingPass); + + public: + AZ_RTTI(HairShortCutGeometryShadingPass, "{11BA673D-0788-4B25-978D-9737BF4E48FE}", HairGeometryRasterPass); + AZ_CLASS_ALLOCATOR(HairShortCutGeometryShadingPass, SystemAllocator, 0); + + static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); + + void CompileResources(const RHI::FrameGraphCompileContext& context) override; + + protected: + AZ::Name o_enableShadows; + AZ::Name o_enableDirectionalLights; + AZ::Name o_enablePunctualLights; + AZ::Name o_enableAreaLights; + AZ::Name o_enableIBL; + AZ::Name o_hairLightingModel; + AZ::Name o_enableMarschner_R; + AZ::Name o_enableMarschner_TRT; + AZ::Name o_enableMarschner_TT; + AZ::Name o_enableLongtitudeCoeff; + AZ::Name o_enableAzimuthCoeff; + + explicit HairShortCutGeometryShadingPass(const RPI::PassDescriptor& descriptor); + + void UpdateGlobalShaderOptions(); + + // Pass behavior overrides + void BuildInternal() override; + + HairGlobalSettings m_hairGlobalSettings; + AZ::RPI::ShaderVariantKey m_shaderOptions; + }; + + } // namespace Hair + } // namespace Render +} // namespace AZ diff --git a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp index 7a317dc09c..b7c6bbce13 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp +++ b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp @@ -47,11 +47,11 @@ namespace AZ HairFeatureProcessor::HairFeatureProcessor() { + m_usePPLLRenderTechnique = false; // Use the ShortCut rendering technique + HairParentPassName = Name{ "HairParentPass" }; - HairPPLLRasterPassName = Name{ "HairPPLLRasterPass" }; - HairPPLLResolvePassName = Name{ "HairPPLLResolvePass" }; - + // Hair Skinning and Simulation Compute passes GlobalShapeConstraintsPassName = Name{ "HairGlobalShapeConstraintsComputePass" }; CalculateStrandDataPassName = Name{ "HairCalculateStrandLevelDataComputePass" }; VelocityShockPropagationPassName = Name{ "HairVelocityShockPropagationComputePass" }; @@ -59,12 +59,21 @@ namespace AZ LengthConstriantsWindAndCollisionPassName = Name{ "HairLengthConstraintsWindAndCollisionComputePass" }; UpdateFollowHairPassName = Name{ "HairUpdateFollowHairComputePass" }; + // PPLL render technique pases + HairPPLLRasterPassName = Name{ "HairPPLLRasterPass" }; + HairPPLLResolvePassName = Name{ "HairPPLLResolvePass" }; + + // ShortCut render technique pases + HairShortCutGeometryDepthAlphaPassName = Name{ "HairShortCutGeometryDepthAlphaPass" }; + HairShortCutResolveDepthPassName = Name{ "HairShortCutResolveDepthPass" }; + HairShortCutGeometryShadingPassName = Name{ "HairShortCutGeometryShadingPass" }; + HairShortCutResolveColorPassName = Name{ "HairShortCutResolveColorPass" }; + ++s_instanceCount; if (!CreatePerPassResources()) { // this might not be an error - if the pass system is still empty / minimal - // and these passes are not part of the minimal pipeline, they will not - // be created. + // and these passes are not part of the minimal pipeline, they will not be created. AZ_Error("Hair Gem", false, "Failed to create the hair shared buffer resource"); } } @@ -127,25 +136,33 @@ namespace AZ m_hairRenderObjects.push_back(renderObject); + // Adding the object will schedule Srgs binding and the DrawItem build for the geometry passes. BuildDispatchAndDrawItems(renderObject); EnablePasses(true); } - void HairFeatureProcessor::EnablePasses(bool enable) + void HairFeatureProcessor::EnablePasses([[maybe_unused]] bool enable) { + return; + + // [To Do] - This part should be enabled (remove the return) to reduce overhead + // when Hair is disabled / doesn't exist in the scene. + // Currently it might break features such as fog that depend on the output and for some + // reason doesn't quite work for ShortCut. + // The current overhead is minimal (< 0.1 msec) and this Gem is disabled by default. +/* if (!m_initialized) { return; } - for (auto& [passName, pass] : m_computePasses) + RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairParentPassName); + if (desiredPass) { - pass->SetEnabled(enable); + desiredPass->SetEnabled(enable); } - - m_hairPPLLRasterPass->SetEnabled(enable); - m_hairPPLLResolvePass->SetEnabled(enable); +*/ } bool HairFeatureProcessor::RemoveHairRenderObject(Data::Instance renderObject) @@ -214,7 +231,8 @@ namespace AZ } if (m_forceRebuildRenderData) - { + { // In the case of a force build, schedule Srgs binding and the DrawItem build for + // the geometry passes of all existing hair objects. for (auto& hairRenderObject : m_hairRenderObjects) { BuildDispatchAndDrawItems(hairRenderObject); @@ -276,17 +294,32 @@ namespace AZ pass->AddDispatchItems(m_hairRenderObjects); } - // Add all hair objects to the Render / Raster Pass - m_hairPPLLRasterPass->AddDrawPackets(m_hairRenderObjects); + if (m_usePPLLRenderTechnique) + { + // Add all hair objects to the Render / Raster Pass + m_hairPPLLRasterPass->AddDrawPackets(m_hairRenderObjects); + } + else + { + m_hairShortCutGeometryDepthAlphaPass->AddDrawPackets(m_hairRenderObjects); + m_hairShortCutGeometryShadingPass->AddDrawPackets(m_hairRenderObjects); + } } void HairFeatureProcessor::ClearPasses() { m_initialized = false; // Avoid simulation or render m_computePasses.clear(); + + // PPLL geometry and resolve full screen passes m_hairPPLLRasterPass = nullptr; m_hairPPLLResolvePass = nullptr; + // ShortCut passes - Special handling of geometry passes only, and using the regular + // full screen pass for the resolve + m_hairShortCutGeometryDepthAlphaPass = nullptr; + m_hairShortCutGeometryShadingPass = nullptr; + // Mark for all passes to evacuate their render data and recreate it. m_forceRebuildRenderData = true; m_forceClearRenderData = true; @@ -338,6 +371,12 @@ namespace AZ ClearPasses(); + if (!m_renderPipeline) + { + AZ_Error("Hair Gem", false, "HairFeatureProcessor does NOT have render pipeline set yet"); + return false; + } + // Compute Passes - populate the passes map bool resultSuccess = InitComputePass(GlobalShapeConstraintsPassName); resultSuccess &= InitComputePass(CalculateStrandDataPassName); @@ -347,8 +386,15 @@ namespace AZ resultSuccess &= InitComputePass(UpdateFollowHairPassName); // Rendering Passes - resultSuccess &= InitPPLLFillPass(); - resultSuccess &= InitPPLLResolvePass(); + if (m_usePPLLRenderTechnique) + { + resultSuccess &= InitPPLLFillPass(); + resultSuccess &= InitPPLLResolvePass(); + } + else + { + resultSuccess &= InitShortCutRenderPasses(); + } m_initialized = resultSuccess; @@ -388,7 +434,8 @@ namespace AZ } } - // PPLL nodes buffer + // PPLL nodes buffer - created only if the PPLL technique is used + if (m_usePPLLRenderTechnique) { descriptor = SrgBufferDescriptor( RPI::CommonBufferPoolType::ReadWrite, RHI::Format::Unknown, @@ -425,11 +472,6 @@ namespace AZ bool HairFeatureProcessor::InitComputePass(const Name& passName, bool allowIterations) { m_computePasses[passName] = nullptr; - if (!m_renderPipeline) - { - AZ_Error("Hair Gem", false, "%s does NOT have render pipeline set yet", passName.GetCStr()); - return false; - } RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(passName); if (desiredPass) @@ -452,11 +494,6 @@ namespace AZ bool HairFeatureProcessor::InitPPLLFillPass() { m_hairPPLLRasterPass = nullptr; // reset it to null, just in case it fails to load the assets properly - if (!m_renderPipeline) - { - AZ_Error("Hair Gem", false, "Hair Fill Pass does NOT have render pipeline set yet"); - return false; - } RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairPPLLRasterPassName); if (desiredPass) @@ -466,7 +503,7 @@ namespace AZ } else { - AZ_Error("Hair Gem", false, "HairPPLLRasterPass does not have any valid passes. Check your game project's .pass assets."); + AZ_Error("Hair Gem", false, "HairPPLLRasterPass cannot be found. Check your game project's .pass assets."); return false; } return true; @@ -475,11 +512,6 @@ namespace AZ bool HairFeatureProcessor::InitPPLLResolvePass() { m_hairPPLLResolvePass = nullptr; // reset it to null, just in case it fails to load the assets properly - if (!m_renderPipeline) - { - AZ_Error("Hair Gem", false, "Hair Fill Pass does NOT have render pipeline set yet"); - return false; - } RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairPPLLResolvePassName); if (desiredPass) @@ -489,12 +521,46 @@ namespace AZ } else { - AZ_Error("Hair Gem", false, "HairPPLLResolvePassTemplate does not have valid passes. Check your game project's .pass assets."); + AZ_Error("Hair Gem", false, "HairPPLLResolvePass cannot be found. Check your game project's .pass assets."); return false; } return true; } + //! Set the two short cut geometry pases and assign them the FP. The other two full screen passes + //! are generic full screen passes and don't need any interaction with the FP. + bool HairFeatureProcessor::InitShortCutRenderPasses() + { + m_hairShortCutGeometryDepthAlphaPass = nullptr; + m_hairShortCutGeometryShadingPass = nullptr; + + m_hairShortCutGeometryDepthAlphaPass = static_cast( + m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairShortCutGeometryDepthAlphaPassName).get()); + if (m_hairShortCutGeometryDepthAlphaPass) + { + m_hairShortCutGeometryDepthAlphaPass->SetFeatureProcessor(this); + } + else + { + AZ_Error("Hair Gem", false, "HairShortCutResolveDepthPass cannot be found. Check your game project's .pass assets."); + return false; + } + + m_hairShortCutGeometryShadingPass = static_cast( + m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairShortCutGeometryShadingPassName).get()); + if (m_hairShortCutGeometryShadingPass) + { + m_hairShortCutGeometryShadingPass->SetFeatureProcessor(this); + } + else + { + AZ_Error("Hair Gem", false, "HairShortCutGeometryShadingPass cannot be found. Check your game project's .pass assets."); + return false; + } + + return true; + } + void HairFeatureProcessor::BuildDispatchAndDrawItems(Data::Instance renderObject) { HairRenderObject* renderObjectPtr = renderObject.get(); @@ -513,9 +579,18 @@ namespace AZ m_computePasses[UpdateFollowHairPassName]->BuildDispatchItem( renderObjectPtr, DispatchLevel::DISPATCHLEVEL_VERTEX); - // Render / Raster pass - adding the object will schedule Srgs binding - // and DrawItem build. - m_hairPPLLRasterPass->SchedulePacketBuild(renderObjectPtr); + // Schedule Srgs binding and the DrawItem build. + // Since this does not bind the PerPass srg but prepare the rest of the Srgs + // such as the dynamic srg, it should only be done once per object per frame. + if (m_usePPLLRenderTechnique) + { + m_hairPPLLRasterPass->SchedulePacketBuild(renderObjectPtr); + } + else + { + m_hairShortCutGeometryDepthAlphaPass->SchedulePacketBuild(renderObjectPtr); + m_hairShortCutGeometryShadingPass->SchedulePacketBuild(renderObjectPtr); + } } Data::Instance HairFeatureProcessor::GetHairSkinningComputegPass() @@ -527,14 +602,28 @@ namespace AZ return m_computePasses[GlobalShapeConstraintsPassName]; } - Data::Instance HairFeatureProcessor::GetHairPPLLRasterPass() + Data::Instance HairFeatureProcessor::GetGeometryRasterShader() { - if (!m_hairPPLLRasterPass) + if (m_usePPLLRenderTechnique) { - Init(m_renderPipeline); + if (!m_hairPPLLRasterPass && !Init(m_renderPipeline)) + { + AZ_Error("Hair Gem", false, + "GetGeometryRasterShader - m_hairPPLLRasterPass was not created"); + return nullptr; + } + return m_hairPPLLRasterPass->GetShader(); } - return m_hairPPLLRasterPass; + + if (!m_hairShortCutGeometryDepthAlphaPass && !Init(m_renderPipeline)) + { + AZ_Error("Hair Gem", false, + "GetGeometryRasterShader - m_hairShortCutGeometryDepthAlphaPass was not created"); + return nullptr; + } + return m_hairShortCutGeometryDepthAlphaPass->GetShader(); } + } // namespace Hair } // namespace Render } // namespace AZ diff --git a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h index c56dc5c921..46660a6623 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h +++ b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h @@ -17,14 +17,19 @@ #include #include +#include // Hair specific #include #include + #include #include +#include +#include + #include #include #include @@ -73,9 +78,7 @@ namespace AZ { Name HairParentPassName; - Name HairPPLLRasterPassName; - Name HairPPLLResolvePassName; - + // Compute passes Name GlobalShapeConstraintsPassName; Name CalculateStrandDataPassName; Name VelocityShockPropagationPassName; @@ -83,6 +86,16 @@ namespace AZ Name LengthConstriantsWindAndCollisionPassName; Name UpdateFollowHairPassName; + // PPLL render passes + Name HairPPLLRasterPassName; + Name HairPPLLResolvePassName; + + // ShortCut render passes + Name HairShortCutGeometryDepthAlphaPassName; + Name HairShortCutResolveDepthPassName; + Name HairShortCutGeometryShadingPassName; + Name HairShortCutResolveColorPassName; + public: AZ_RTTI(AZ::Render::Hair::HairFeatureProcessor, "{5F9DDA81-B43F-4E30-9E56-C7C3DC517A4C}", RPI::FeatureProcessor); @@ -117,6 +130,7 @@ namespace AZ Data::Instance GetHairSkinningComputegPass(); Data::Instance GetHairPPLLRasterPass(); + Data::Instance GetGeometryRasterShader(); //! Update the hair objects materials array. void FillHairMaterialsArray(std::vector& renderSettings); @@ -144,6 +158,7 @@ namespace AZ bool InitPPLLFillPass(); bool InitPPLLResolvePass(); + bool InitShortCutRenderPasses(); bool InitComputePass(const Name& passName, bool allowIterations = false); void BuildDispatchAndDrawItems(Data::Instance renderObject); @@ -168,10 +183,14 @@ namespace AZ //! Simulation Compute Passes AZStd::unordered_map > m_computePasses; - // Render Passes + // PPLL Render Passes Data::Instance m_hairPPLLRasterPass = nullptr; Data::Instance m_hairPPLLResolvePass = nullptr; + // ShortCut Render Passes - special case for the geometry render passes + Data::Instance m_hairShortCutGeometryDepthAlphaPass = nullptr; + Data::Instance m_hairShortCutGeometryShadingPass = nullptr; + //-------------------------------------------------------------- // Per Pass Resources //-------------------------------------------------------------- @@ -196,6 +215,7 @@ namespace AZ bool m_forceClearRenderData = false; bool m_initialized = false; bool m_isEnabled = true; + bool m_usePPLLRenderTechnique = true; static uint32_t s_instanceCount; HairGlobalSettings m_hairGlobalSettings; diff --git a/Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp b/Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp index baf986daf6..4d615e31ae 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp +++ b/Gems/AtomTressFX/Code/Rendering/HairRenderObject.cpp @@ -993,7 +993,7 @@ namespace AZ //------------------------------------- // Dynamic buffers, data and Srg creation - shared between passes and changed on the GPU if (!m_dynamicHairData.CreateDynamicGPUResources( - m_skinningShader, m_PPLLFillShader, + m_skinningShader, m_geometryRasterShader, m_NumTotalVertices, m_NumTotalStrands)) { AZ_Error("Hair Gem", false, "Hair - Error creating dynamic resources [%s]", assetName ); @@ -1028,7 +1028,7 @@ namespace AZ // Rendering setup bool renderResourcesSuccess; - renderResourcesSuccess = CreateRenderingGPUResources(m_PPLLFillShader, *asset, assetName); + renderResourcesSuccess = CreateRenderingGPUResources(m_geometryRasterShader, *asset, assetName); renderResourcesSuccess &= PopulateDrawStrandsBindSet(renderSettings); renderResourcesSuccess &= UploadRenderingGPUResources(*asset); @@ -1057,17 +1057,10 @@ namespace AZ } { - Data::Instance rasterPass = m_featureProcessor->GetHairPPLLRasterPass(); - if (!rasterPass.get()) + m_geometryRasterShader = m_featureProcessor->GetGeometryRasterShader(); + if (!m_geometryRasterShader) { - AZ_Error("Hair Gem", false, "Failed to get PPLL raster fill Pass."); - return false; - } - - m_PPLLFillShader = rasterPass->GetShader(); - if (!m_PPLLFillShader) - { - AZ_Error("Hair Gem", false, "Failed to get hair raster fill shader from raster pass"); + AZ_Error("Hair Gem", false, "Failed to get hair geometry raster shader"); return false; } } @@ -1116,7 +1109,7 @@ namespace AZ return updatedCB; } - bool HairRenderObject::BuildPPLLDrawPacket(RHI::DrawPacketBuilder::DrawRequest& drawRequest) + bool HairRenderObject::BuildDrawPacket(RPI::Shader* geometryShader, RHI::DrawPacketBuilder::DrawRequest& drawRequest) { RHI::DrawPacketBuilder drawPacketBuilder; RHI::DrawIndexed drawIndexed; @@ -1159,21 +1152,38 @@ namespace AZ drawPacketBuilder.AddShaderResourceGroup(simSrg->GetRHIShaderResourceGroup()); drawPacketBuilder.AddDrawItem(drawRequest); - if (m_fillDrawPacket) - { - delete m_fillDrawPacket; - } - m_fillDrawPacket = drawPacketBuilder.End(); - - if (!m_fillDrawPacket) + const RHI::DrawPacket* drawPacket = drawPacketBuilder.End(); + if (!drawPacket) { AZ_Error("Hair Gem", false, "Failed to build the hair DrawPacket."); return false; } + // Insert the newly created draw packet to the map based on its shader + auto iter = m_geometryDrawPackets.find(geometryShader); + if (iter != m_geometryDrawPackets.end()) + { + delete iter->second; + iter->second = drawPacket; + } + else + { + m_geometryDrawPackets[geometryShader] = drawPacket; + } + return true; } + const RHI::DrawPacket* HairRenderObject::GetGeometrylDrawPacket(RPI::Shader* geometryShader) + { + auto iter = m_geometryDrawPackets.find(geometryShader); + if (iter == m_geometryDrawPackets.end()) + { + return nullptr; + } + return iter->second; + } + const RHI::DispatchItem* HairRenderObject::GetDispatchItem(RPI::Shader* computeShader) { auto dispatchIter = m_dispatchItems.find(computeShader); @@ -1210,4 +1220,3 @@ namespace AZ } // namespace Hair } // namespace Render } // namespace AZ - diff --git a/Gems/AtomTressFX/Code/Rendering/HairRenderObject.h b/Gems/AtomTressFX/Code/Rendering/HairRenderObject.h index fa4095eed0..817ebd89fa 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairRenderObject.h +++ b/Gems/AtomTressFX/Code/Rendering/HairRenderObject.h @@ -179,14 +179,9 @@ namespace AZ AMD::TressFXSimulationSettings* simSettings, AMD::TressFXRenderingSettings* renderSettings ); - //! Creates and fill the draw item associated with the PPLL render of the - //! current hair object - const RHI::DrawPacket* GetFillDrawPacket() - { - return m_fillDrawPacket; - } + bool BuildDrawPacket(RPI::Shader* geometryShader, RHI::DrawPacketBuilder::DrawRequest& drawRequest); - bool BuildPPLLDrawPacket(RHI::DrawPacketBuilder::DrawRequest& drawRequest); + const RHI::DrawPacket* GetGeometrylDrawPacket(RPI::Shader* geometryShader); //! Creates and fill the dispatch item associated with the compute shader bool BuildDispatchItem(RPI::Shader* computeShader, DispatchLevel dispatchLevel); @@ -302,17 +297,20 @@ namespace AZ //! responsible for the various stages and passes' updates HairFeatureProcessor* m_featureProcessor = nullptr; - //! The dispatch item used for the skinning - HairDispatchItem m_skinningDispatchItem; - - //! Compute dispatch items map per the existing passes - AZStd::map> m_dispatchItems; - //! Skinning compute shader used for creation of the compute Srgs and dispatch item Data::Instance m_skinningShader = nullptr; - //! PPLL fill shader used for creation of the raster Srgs and draw item - Data::Instance m_PPLLFillShader = nullptr; + //! Compute dispatch items map per the existing passes + AZStd::unordered_map> m_dispatchItems; + + //! Geometry raster shader used for creation of the raster Srgs. + //! Since the Srgs for geometry raster are the same across the shaders we keep + //! only a single shader - if this to change in the future, several shaders and sets + //! of dynamic Srgs should be created. + Data::Instance m_geometryRasterShader = nullptr; + + //! DrawPacket for the multi object geometry raster pass. + AZStd::unordered_map m_geometryDrawPackets; float m_frameDeltaTime = 0.02; @@ -378,9 +376,6 @@ namespace AZ //! Index buffer for the render pass via draw calls - naming was kept Data::Instance m_indexBuffer; RHI::IndexBufferView m_indexBufferView; - - //! DrawPacket for the multi object raster fill pass. - const RHI::DrawPacket* m_fillDrawPacket = nullptr; //------------------------------------------------------------------- AZStd::mutex m_mutex; diff --git a/Gems/AtomTressFX/Hair_files.cmake b/Gems/AtomTressFX/Hair_files.cmake index 000abeb559..180685d499 100644 --- a/Gems/AtomTressFX/Hair_files.cmake +++ b/Gems/AtomTressFX/Hair_files.cmake @@ -67,6 +67,13 @@ set(FILES # Base class of all geometry raster passes Code/Passes/HairGeometryRasterPass.h Code/Passes/HairGeometryRasterPass.cpp + + # ShortCut rendering technique - pass classes + Code/Passes/HairShortCutGeometryDepthAlphaPass.h + Code/Passes/HairShortCutGeometryDepthAlphaPass.cpp + Code/Passes/HairShortCutGeometryShadingPass.h + Code/Passes/HairShortCutGeometryShadingPass.cpp + # PPLL rendering technique - geometry raster pass Code/Passes/HairPPLLRasterPass.h Code/Passes/HairPPLLRasterPass.cpp @@ -84,30 +91,37 @@ set(FILES Code/Assets/HairAsset.cpp #) #set(shaders_sources - # Srgs and Utility files - Assets/Shaders/HairSrgs.azsli - Assets/Shaders/HairSimulationSrgs.azsli + # Geometry and Full Screen azsl utility files Assets/Shaders/HairRenderingSrgs.azsli - Assets/Shaders/HairSimulationCommon.azsli Assets/Shaders/HairStrands.azsli Assets/Shaders/HairUtilities.azsli + Assets/Shaders/HairFullScreenUtils.azsli Assets/Shaders/HairLighting.azsli Assets/Shaders/HairLightingEquations.azsli Assets/Shaders/HairLightTypes.azsli Assets/Shaders/HairSurface.azsli - # Simulation Compute shaders - Assets/Shaders/HairSimulationCompute.azsl - - # Collision shaders - to be included soon -# Assets/Shaders/HairCollisionPrepareSDF.azsl -# Assets/Shaders/HairCollisionWithSDF.azsl + # ShortCut technique shaders (using multiple RTs instead of PPLL for GPU memory reduction) + Assets/Shaders/HairShortCutGeometryDepthAlpha.azsl + Assets/Shaders/HairShortCutResolveDepth.azsl + Assets/Shaders/HairShortCutGeometryShading.azsl + Assets/Shaders/HairShortCutResolveColor.azsl - # Rendering shaders + # Rendering azsl files Assets/Shaders/HairRenderingFillPPLL.azsl Assets/Shaders/HairRenderingResolvePPLL.azsl - # Simulation .shader files + # Simulation Compute azsl files + Assets/Shaders/HairComputeSrgs.azsli + Assets/Shaders/HairSimulationComputeSrgs.azsli + Assets/Shaders/HairSimulationCommon.azsli + Assets/Shaders/HairSimulationCompute.azsl + + # Collision azsl files - to be included soon +# Assets/Shaders/HairCollisionPrepareSDF.azsl +# Assets/Shaders/HairCollisionWithSDF.azsl + + # Simulation Compute .shader files Assets/Shaders/HairGlobalShapeConstraintsCompute.shader Assets/Shaders/HairCalculateStrandLevelDataCompute.shader Assets/Shaders/HairVelocityShockPropagationCompute.shader @@ -115,9 +129,15 @@ set(FILES Assets/Shaders/HairLengthConstraintsWindAndCollisionCompute.shader Assets/Shaders/HairUpdateFollowHairCompute.shader - # Rendering .shader file + # PPLL Render .shader file Assets/Shaders/HairRenderingFillPPLL.shader Assets/Shaders/HairRenderingResolvePPLL.shader + + # ShortCut Render .shader file + Assets/Shaders/HairShortCutGeometryDepthAlpha.shader + Assets/Shaders/HairShortCutResolveDepth.shader + Assets/Shaders/HairShortCutGeometryShading.shader + Assets/Shaders/HairShortCutResolveColor.shader # Colisions .shader files - to be included soon # Assets/Shaders/HairCollisionInitializeSDF.shader @@ -127,15 +147,25 @@ set(FILES #) # #set(atom_hair_passes + # Compute simulation and skinning passes Assets/Passes/HairParentPass.pass + Assets/Passes/HairParentShortCutPass.pass Assets/Passes/HairGlobalShapeConstraintsCompute.pass Assets/Passes/HairCalculateStrandLevelDataCompute.pass Assets/Passes/HairVelocityShockPropagationCompute.pass Assets/Passes/HairLocalShapeConstraintsCompute.pass Assets/Passes/HairLengthConstraintsWindAndCollisionCompute.pass Assets/Passes/HairUpdateFollowHairCompute.pass + + # PPLL render passes Assets/Passes/HairFillPPLL.pass Assets/Passes/HairResolvePPLL.pass + + # Shortcut render passes + Assets/Passes/HairShortCutGeometryDepthAlpha.pass + Assets/Passes/HairShortCutResolveDepth.pass + Assets/Passes/HairShortCutGeometryShading.pass + Assets/Passes/HairShortCutResolveColor.pass ) set(SKIP_UNITY_BUILD_INCLUSION_FILES From 68b585102e5c8b64254683c78e734576b20910de Mon Sep 17 00:00:00 2001 From: hershey5045 <43485729+hershey5045@users.noreply.github.com> Date: Thu, 21 Oct 2021 12:06:46 -0700 Subject: [PATCH 05/10] Re-order some color grading operations. Increase values for SMH to account for HDR colors. (#4856) Signed-off-by: rbarrand Co-authored-by: rbarrand --- .../PostProcessing/HDRColorGradingCommon.azsl | 5 ++--- .../ColorGrading/EditorHDRColorGradingComponent.cpp | 12 ++++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl index 099a6394d4..a380a0a547 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl @@ -138,7 +138,7 @@ float3 ColorGrade(float3 frameColor) PassSrg::m_colorFilterMultiply, PassSrg::m_colorFilterIntensity), PassSrg::m_colorAdjustmentWeight); frameColor = max(frameColor, 0.0); frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPreSaturation), PassSrg::m_colorAdjustmentWeight); - + frameColor = max(frameColor, 0.0); frameColor = ColorGradeSplitTone(frameColor, PassSrg::m_splitToneBalance, PassSrg::m_splitToneWeight, PassSrg::m_splitToneShadowsColor, PassSrg::m_splitToneHighlightsColor); frameColor = ColorGradeChannelMixer(frameColor, PassSrg::m_channelMixingRed, PassSrg::m_channelMixingGreen, PassSrg::m_channelMixingBlue); @@ -147,8 +147,7 @@ float3 ColorGrade(float3 frameColor) PassSrg::m_smhHighlightsStart, PassSrg::m_smhHighlightsEnd, PassSrg::m_smhWeight, PassSrg::m_smhShadowsColor, PassSrg::m_smhMidtonesColor, PassSrg::m_smhHighlightsColor); - - frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation), PassSrg::m_finalAdjustmentWeight); frameColor = lerp(frameColor, ColorGradeHueShift(frameColor, PassSrg::m_colorGradingHueShift), PassSrg::m_finalAdjustmentWeight); + frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation), PassSrg::m_finalAdjustmentWeight); return max(frameColor.rgb, 0.0); } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp index 2d8f83a32a..e32b26f0c5 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp @@ -131,16 +131,20 @@ namespace AZ ->Attribute(Edit::Attributes::Max, 1.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhShadowsStart, "Shadows Start", "SMH Shadows Start Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhShadowsEnd, "Shadows End", "SMH Shadows End Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhHighlightsStart, "Highlights Start", "SMH Highlights Start Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhHighlightsEnd, "Highlights End", "SMH Highlights End Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhShadowsColor, "Shadows Color", "SMH Shadows Color") ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhMidtonesColor, "Midtones Color", "SMH Midtones Color") ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhHighlightsColor, "Highlights Color", "SMH Highlights Color") From 731680294138c193c69d0f1f654f4478df5f74d3 Mon Sep 17 00:00:00 2001 From: chiyenteng <82238204+chiyenteng@users.noreply.github.com> Date: Thu, 21 Oct 2021 12:08:00 -0700 Subject: [PATCH 06/10] Add Detach and Duplicate Prefab basic workflow auto test (#4506) - Add a new automated test PrefabBasicWorkflow_CreateReparentAndDetachPrefab for verifying prefab detachment basic workflow. - Add a new automated test PrefabBasicWorkflow_CreateAndDuplicatePrefab for verifying prefab detachment basic workflow. - Fix a bug related to sets of entity ids in Reparent helper function . --- .../editor_python_test_tools/prefab_utils.py | 206 ++++++++++++++---- .../Gem/PythonTests/Prefab/TestSuite_Main.py | 8 + ...efabBasicWorkflow_CreateAndDeletePrefab.py | 7 +- ...bBasicWorkflow_CreateAndDuplicatePrefab.py | 32 +++ ...abBasicWorkflow_CreateAndReparentPrefab.py | 11 +- .../tests/PrefabBasicWorkflow_CreatePrefab.py | 5 +- ...cWorkflow_CreateReparentAndDetachPrefab.py | 51 +++++ .../PrefabBasicWorkflow_InstantiatePrefab.py | 3 +- .../Prefab/tests/PrefabTestUtils.py | 27 --- .../Prefab/EditorPrefabComponent.cpp | 9 + .../Prefab/EditorPrefabComponent.h | 4 +- .../Prefab/PrefabPublicHandler.cpp | 9 +- .../Prefab/PrefabPublicHandler.h | 2 +- .../Prefab/PrefabPublicInterface.h | 8 +- .../Prefab/PrefabPublicRequestBus.h | 24 ++ .../Prefab/PrefabPublicRequestHandler.cpp | 17 ++ .../Prefab/PrefabPublicRequestHandler.h | 3 + .../AzToolsFramework/Prefab/PrefabUndo.cpp | 2 +- .../AzToolsFramework/Prefab/PrefabUndo.h | 2 +- 19 files changed, 328 insertions(+), 102 deletions(-) create mode 100644 AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py create mode 100644 AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py index 2d8b124125..10a6ab1ef4 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py @@ -9,6 +9,7 @@ from __future__ import annotations from collections import Counter from collections import deque from os import path +from pathlib import Path from PySide2 import QtWidgets @@ -20,6 +21,10 @@ from editor_python_test_tools.utils import Report import azlmbr.entity as entity import azlmbr.bus as bus +import azlmbr.components as components +import azlmbr.editor as editor +import azlmbr.globals +import azlmbr.math as math import azlmbr.prefab as prefab import editor_python_test_tools.pyside_utils as pyside_utils @@ -57,26 +62,46 @@ class PrefabInstance: def __hash__(self): return hash(self.container_entity.id) - """ - See if this instance is valid to be used with other prefab operations. - :return: Whether the target instance is valid or not. - """ def is_valid(self) -> bool: + """ + See if this instance is valid to be used with other prefab operations. + :return: Whether the target instance is valid or not. + """ return self.container_entity.id.IsValid() and self.prefab_file_name in Prefab.existing_prefabs - """ - Reparent this instance to target parent entity. - The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs. - :param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next. - """ + def has_editor_prefab_component(self) -> bool: + """ + Check if the instance's container entity contains EditorPrefabComponent. + :return: Whether the container entity of target instance has EditorPrefabComponent in it or not. + """ + return editor.EditorComponentAPIBus(bus.Broadcast, "HasComponentOfType", self.container_entity.id, azlmbr.globals.property.EditorPrefabComponentTypeId) + + def is_at_position(self, expected_position): + """ + Check if the instance's container entity is at expected position given. + :return: Whether the container entity of target instance is at expected position or not. + """ + actual_position = components.TransformBus(bus.Event, "GetWorldTranslation", self.container_entity.id) + is_at_position = actual_position.IsClose(expected_position) + + if not is_at_position: + Report.info(f"Prefab Instance Container Entity '{self.container_entity.id.ToString()}'\'s expected position: {expected_position.ToString()}, actual position: {actual_position.ToString()}") + + return is_at_position + async def ui_reparent_prefab_instance(self, parent_entity_id: EntityId): + """ + Reparent this instance to target parent entity. + The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs. + :param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next. + """ container_entity_id_before_reparent = self.container_entity.id original_parent = EditorEntity(self.container_entity.get_parent_id()) - original_parent_before_reparent_children_ids = set(original_parent.get_children_ids()) + original_parent_before_reparent_children_ids = {child_id.ToString(): child_id for child_id in original_parent.get_children_ids()} new_parent = EditorEntity(parent_entity_id) - new_parent_before_reparent_children_ids = set(new_parent.get_children_ids()) + new_parent_before_reparent_children_ids = {child_id.ToString(): child_id for child_id in new_parent.get_children_ids()} pyside_utils.run_soon(lambda: self.container_entity.set_parent_entity(parent_entity_id)) pyside_utils.run_soon(lambda: wait_for_propagation()) @@ -90,23 +115,28 @@ class PrefabInstance: except pyside_utils.EventLoopTimeoutException: pass - original_parent_after_reparent_children_ids = set(original_parent.get_children_ids()) + original_parent_after_reparent_children_ids = {child_id.ToString(): child_id for child_id in original_parent.get_children_ids()} assert len(original_parent_after_reparent_children_ids) == len(original_parent_before_reparent_children_ids) - 1, \ "The children count of the Prefab Instance's original parent should be decreased by 1." assert not container_entity_id_before_reparent in original_parent_after_reparent_children_ids, \ "This Prefab Instance is still a child entity of its original parent entity." - new_parent_after_reparent_children_ids = set(new_parent.get_children_ids()) + new_parent_after_reparent_children_ids = {child_id.ToString(): child_id for child_id in new_parent.get_children_ids()} assert len(new_parent_after_reparent_children_ids) == len(new_parent_before_reparent_children_ids) + 1, \ "The children count of the Prefab Instance's new parent should be increased by 1." - container_entity_id_after_reparent = set(new_parent_after_reparent_children_ids).difference(new_parent_before_reparent_children_ids).pop() + after_before_diff = set(new_parent_after_reparent_children_ids.keys()).difference(set(new_parent_before_reparent_children_ids.keys())) + container_entity_id_after_reparent = new_parent_after_reparent_children_ids[after_before_diff.pop()] reparented_container_entity = EditorEntity(container_entity_id_after_reparent) reparented_container_entity_parent_id = reparented_container_entity.get_parent_id() has_correct_parent = reparented_container_entity_parent_id.ToString() == parent_entity_id.ToString() assert has_correct_parent, "Prefab Instance reparented is *not* under the expected parent entity" + current_instance_prefab = Prefab.get_prefab(self.prefab_file_name) + current_instance_prefab.instances.remove(self) + self.container_entity = reparented_container_entity + current_instance_prefab.instances.add(self) # This is a helper class which contains some of the useful information about a prefab template. class Prefab: @@ -117,31 +147,32 @@ class Prefab: self.file_path: str = get_prefab_file_path(file_path) self.instances: set[PrefabInstance] = set() - """ - Check if a prefab is ready to be used to generate its instances. - :param file_path: A unique file path of the target prefab. - :return: Whether the target prefab is loaded or not. - """ @classmethod def is_prefab_loaded(cls, file_path: str) -> bool: + """ + Check if a prefab is ready to be used to generate its instances. + :param file_path: A unique file path of the target prefab. + :return: Whether the target prefab is loaded or not. + """ return file_path in Prefab.existing_prefabs - """ - Check if a prefab exists in the directory for files of prefab tests. - :param file_name: A unique file name of the target prefab. - :return: Whether the target prefab exists or not. - """ + @classmethod def prefab_exists(cls, file_path: str) -> bool: + """ + Check if a prefab exists in the directory for files of prefab tests. + :param file_name: A unique file name of the target prefab. + :return: Whether the target prefab exists or not. + """ return path.exists(get_prefab_file_path(file_path)) - """ - Return a prefab which can be used immediately. - :param file_name: A unique file name of the target prefab. - :return: The prefab with given file name. - """ @classmethod def get_prefab(cls, file_name: str) -> Prefab: + """ + Return a prefab which can be used immediately. + :param file_name: A unique file name of the target prefab. + :return: The prefab with given file name. + """ assert file_name, "Received an empty file_name" if Prefab.is_prefab_loaded(file_name): return Prefab.existing_prefabs[file_name] @@ -151,15 +182,15 @@ class Prefab: Prefab.existing_prefabs[file_name] = Prefab(file_name) return new_prefab - """ - Create a prefab in memory and return it. The very first instance of this prefab will also be created. - :param entities: The entities that should form the new prefab (along with their descendants). - :param file_name: A unique file name of new prefab. - :param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name. - :return: Created Prefab object and the very first PrefabInstance object owned by the prefab. - """ @classmethod def create_prefab(cls, entities: list[EditorEntity], file_name: str, prefab_instance_name: str=None) -> tuple(Prefab, PrefabInstance): + """ + Create a prefab in memory and return it. The very first instance of this prefab will also be created. + :param entities: The entities that should form the new prefab (along with their descendants). + :param file_name: A unique file name of new prefab. + :param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name. + :return: Created Prefab object and the very first PrefabInstance object owned by the prefab. + """ assert not Prefab.is_prefab_loaded(file_name), f"Can't create Prefab '{file_name}' since the prefab already exists" new_prefab = Prefab(file_name) @@ -169,6 +200,9 @@ class Prefab: container_entity_id = create_prefab_result.GetValue() container_entity = EditorEntity(container_entity_id) + children_entity_ids = container_entity.get_children_ids() + + assert len(children_entity_ids) == len(entities), f"Entity count of created prefab instance does *not* match the count of given entities." if prefab_instance_name: container_entity.set_name(prefab_instance_name) @@ -180,12 +214,12 @@ class Prefab: Prefab.existing_prefabs[file_name] = new_prefab return new_prefab, new_prefab_instance - """ - Remove target prefab instances. - :param prefab_instances: Instances to be removed. - """ @classmethod def remove_prefabs(cls, prefab_instances: list[PrefabInstance]): + """ + Remove target prefab instances. + :param prefab_instances: Instances to be removed. + """ entity_ids_to_remove = [] entity_id_queue = [prefab_instance.container_entity for prefab_instance in prefab_instances] while entity_id_queue: @@ -212,15 +246,89 @@ class Prefab: instance_deleted_prefab = Prefab.get_prefab(instance.prefab_file_name) instance_deleted_prefab.instances.remove(instance) instance = PrefabInstance() - - """ - Instantiate an instance of this prefab. - :param parent_entity: The entity the prefab should be a child of in the transform hierarchy. - :param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name. - :param prefab_position: The position in world space the prefab should be instantiated in. - :return: Instantiated PrefabInstance object owned by this prefab. - """ + + @classmethod + def duplicate_prefabs(cls, prefab_instances: list[PrefabInstance]): + """ + Duplicate target prefab instances. + :param prefab_instances: Instances to be duplicated. + :return: PrefabInstance objects of given prefab instances' duplicates. + """ + assert prefab_instances, "Input list of prefab instances should *not* be empty." + + common_parent = EditorEntity(prefab_instances[0].container_entity.get_parent_id()) + common_parent_children_ids_before_duplicate = set([child_id.ToString() for child_id in common_parent.get_children_ids()]) + + container_entity_ids = [prefab_instance.container_entity.id for prefab_instance in prefab_instances] + + duplicate_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DuplicateEntitiesInInstance', container_entity_ids) + assert duplicate_prefab_result.IsSuccess(), f"Prefab operation 'DuplicateEntitiesInInstance' failed. Error: {duplicate_prefab_result.GetError()}" + + wait_for_propagation() + + duplicate_container_entity_ids = duplicate_prefab_result.GetValue() + common_parent_children_ids_after_duplicate = set([child_id.ToString() for child_id in common_parent.get_children_ids()]) + + assert set([container_entity_id.ToString() for container_entity_id in container_entity_ids]).issubset(common_parent_children_ids_after_duplicate), \ + "Provided prefab instances are *not* the children of their common parent anymore after duplication." + assert common_parent_children_ids_before_duplicate.issubset(common_parent_children_ids_after_duplicate), \ + "Some children of provided entities' common parent before duplication are *not* the children of the common parent anymore after duplication." + assert len(common_parent_children_ids_after_duplicate) == len(common_parent_children_ids_before_duplicate) + len(prefab_instances), \ + "The children count of the given prefab instances' common parent entity is *not* increased to the expected number." + assert EditorEntity(duplicate_container_entity_ids[0]).get_parent_id().ToString() == common_parent.id.ToString(), \ + "Provided prefab instances' parent should be the same as duplicates' parent." + + duplicate_instances = [] + for duplicate_container_entity_id in duplicate_container_entity_ids: + prefab_file_path = prefab.PrefabPublicRequestBus(bus.Broadcast, 'GetOwningInstancePrefabPath', duplicate_container_entity_id) + assert prefab_file_path, "Returned file path should *not* be empty." + + prefab_file_name = Path(prefab_file_path).stem + duplicate_instance_prefab = Prefab.get_prefab(prefab_file_name) + duplicate_instance = PrefabInstance(prefab_file_path, EditorEntity(duplicate_container_entity_id)) + duplicate_instance_prefab.instances.add(duplicate_instance) + duplicate_instances.append(duplicate_instance) + + return duplicate_instances + + @classmethod + def detach_prefab(cls, prefab_instance: PrefabInstance): + """ + Detach target prefab instance. + :param prefab_instances: Instance to be detached. + """ + parent = EditorEntity(prefab_instance.container_entity.get_parent_id()) + parent_children_ids_before_detach = set([child_id.ToString() for child_id in parent.get_children_ids()]) + + assert prefab_instance.has_editor_prefab_component(), f"Container entity should have EditorPrefabComponent before detachment." + + detach_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DetachPrefab', prefab_instance.container_entity.id) + assert detach_prefab_result.IsSuccess(), f"Prefab operation 'DetachPrefab' failed. Error: {detach_prefab_result.GetError()}" + + assert not prefab_instance.has_editor_prefab_component(), f"Container entity should *not* have EditorPrefabComponent after detachment." + + parent_children_ids_after_detach = set([child_id.ToString() for child_id in parent.get_children_ids()]) + + assert prefab_instance.container_entity.id.ToString() in parent_children_ids_after_detach, \ + "Target prefab instance's container entity id should still exists after the detachment and before the propagation." + + assert len(parent_children_ids_after_detach) == len(parent_children_ids_before_detach), \ + "Parent entity should still keep the same amount of children entities." + + wait_for_propagation() + + instance_owner_prefab = Prefab.get_prefab(prefab_instance.prefab_file_name) + instance_owner_prefab.instances.remove(prefab_instance) + prefab_instance = PrefabInstance() + def instantiate(self, parent_entity: EditorEntity=None, name: str=None, prefab_position: Vector3=Vector3()) -> PrefabInstance: + """ + Instantiate an instance of this prefab. + :param parent_entity: The entity the prefab should be a child of in the transform hierarchy. + :param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name. + :param prefab_position: The position in world space the prefab should be instantiated in. + :return: Instantiated PrefabInstance object owned by this prefab. + """ parent_entity_id = parent_entity.id if parent_entity is not None else EntityId() instantiate_prefab_result = prefab.PrefabPublicRequestBus( @@ -240,4 +348,6 @@ class Prefab: assert not new_prefab_instance in self.instances, "This prefab instance is already existed before this instantiation." self.instances.add(new_prefab_instance) + assert new_prefab_instance.is_at_position(prefab_position), "This prefab instance is *not* at expected position." + return new_prefab_instance diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py index 30a0055d5a..5337f0669c 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py @@ -47,3 +47,11 @@ class TestAutomation(TestAutomationBase): def test_PrefabBasicWorkflow_CreateAndReparentPrefab(self, request, workspace, editor, launcher_platform): from .tests import PrefabBasicWorkflow_CreateAndReparentPrefab as test_module self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False) + + def test_PrefabBasicWorkflow_CreateReparentAndDetachPrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_CreateReparentAndDetachPrefab as test_module + self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False) + + def test_PrefabBasicWorkflow_CreateAndDuplicatePrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_CreateAndDuplicatePrefab as test_module + self._run_prefab_test(request, workspace, editor, test_module) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py index 9ae4614d80..bbebd70e04 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py @@ -16,16 +16,15 @@ def PrefabBasicWorkflow_CreateAndDeletePrefab(): prefab_test_utils.open_base_tests_level() - # Creates a new Entity at the root level - # Asserts if creation didn't succeed + # Creates a new entity at the root level car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Asserts if prefab creation doesn't succeeds + # Creates a prefab from the new entity _, car = Prefab.create_prefab( car_prefab_entities, CAR_PREFAB_FILE_NAME) - # Asserts if prefab deletion fails + # Deletes the prefab instance Prefab.remove_prefabs([car]) if __name__ == "__main__": diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py new file mode 100644 index 0000000000..2479ae549e --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py @@ -0,0 +1,32 @@ +""" +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 +""" + +def PrefabBasicWorkflow_CreateAndDuplicatePrefab(): + + CAR_PREFAB_FILE_NAME = 'car_prefab' + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from editor_python_test_tools.prefab_utils import Prefab + + import PrefabTestUtils as prefab_test_utils + + prefab_test_utils.open_base_tests_level() + + # Creates a new entity at the root level + car_entity = EditorEntity.create_editor_entity() + car_prefab_entities = [car_entity] + + # Creates a prefab from the new entity + _, car = Prefab.create_prefab( + car_prefab_entities, CAR_PREFAB_FILE_NAME) + + # Duplicates the prefab instance + Prefab.duplicate_prefabs([car]) + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(PrefabBasicWorkflow_CreateAndDuplicatePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py index f78bbf483d..1cbc591c29 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py @@ -22,24 +22,23 @@ def PrefabBasicWorkflow_CreateAndReparentPrefab(): prefab_test_utils.open_base_tests_level() - # Creates a new Entity at the root level - # Asserts if creation didn't succeed + # Creates a new car entity at the root level car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Checks for prefab creation passed or not + # Creates a prefab from the car entity _, car = Prefab.create_prefab( car_prefab_entities, CAR_PREFAB_FILE_NAME) - # Creates another new Entity at the root level + # Creates another new wheel entity at the root level wheel_entity = EditorEntity.create_editor_entity() wheel_prefab_entities = [wheel_entity] - # Checks for wheel prefab creation passed or not + # Creates another prefab from the wheel entity _, wheel = Prefab.create_prefab( wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME) - # Checks for prefab reparenting passed or not + # Reparents the wheel prefab instance to the container entity of the car prefab instance await wheel.ui_reparent_prefab_instance(car.container_entity.id) run_test() diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py index 568a2c15b4..cae105a9a9 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py @@ -17,12 +17,11 @@ def PrefabBasicWorkflow_CreatePrefab(): prefab_test_utils.open_base_tests_level() - # Creates a new Entity at the root level - # Asserts if creation didn't succeed + # Creates a new entity at the root level car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Checks for prefab creation passed or not + # Creates a prefab from the new entity Prefab.create_prefab(car_prefab_entities, CAR_PREFAB_FILE_NAME) if __name__ == "__main__": diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py new file mode 100644 index 0000000000..bdf77c4bf3 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py @@ -0,0 +1,51 @@ +""" +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 +""" + +def PrefabBasicWorkflow_CreateReparentAndDetachPrefab(): + + CAR_PREFAB_FILE_NAME = 'car_prefab' + WHEEL_PREFAB_FILE_NAME = 'wheel_prefab' + + import editor_python_test_tools.pyside_utils as pyside_utils + + @pyside_utils.wrap_async + async def run_test(): + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from editor_python_test_tools.prefab_utils import Prefab + + import PrefabTestUtils as prefab_test_utils + + prefab_test_utils.open_base_tests_level() + + # Creates a new car entity at the root level + car_entity = EditorEntity.create_editor_entity() + car_prefab_entities = [car_entity] + + # Creates a prefab from the car entity + _, car = Prefab.create_prefab( + car_prefab_entities, CAR_PREFAB_FILE_NAME) + + # Creates another new wheel entity at the root level + wheel_entity = EditorEntity.create_editor_entity() + wheel_prefab_entities = [wheel_entity] + + # Creates another prefab from the wheel entity + _, wheel = Prefab.create_prefab( + wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME) + + # Reparents the wheel prefab instance to the container entity of the car prefab instance + await wheel.ui_reparent_prefab_instance(car.container_entity.id) + + # Detaches the wheel prefab instance + Prefab.detach_prefab(wheel) + + run_test() + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(PrefabBasicWorkflow_CreateReparentAndDetachPrefab) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py index 1b962d2ca7..a701802cd4 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py @@ -19,9 +19,8 @@ def PrefabBasicWorkflow_InstantiatePrefab(): prefab_test_utils.open_base_tests_level() - # Checks for prefab instantiation passed or not + # Instantiates a new car prefab instance test_prefab = Prefab.get_prefab(EXISTING_TEST_PREFAB_FILE_NAME) - test_instance = test_prefab.instantiate( prefab_position=INSTANTIATED_TEST_PREFAB_POSITION) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py index f82af23023..f865daf41a 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py @@ -18,20 +18,6 @@ import azlmbr.components as components import azlmbr.entity as entity import azlmbr.legacy.general as general -def check_entity_at_position(entity_id, expected_entity_position): - entity_at_expected_position_result = ( - "entity is at expected position", - "entity is *not* at expected position") - - actual_entity_position = components.TransformBus(bus.Event, "GetWorldTranslation", entity_id) - is_at_position = actual_entity_position.IsClose(expected_entity_position) - Report.result(entity_at_expected_position_result, is_at_position) - - if not is_at_position: - Report.info(f"Entity '{entity_id.ToString()}'\'s expected position: {expected_entity_position.ToString()}, actual position: {actual_entity_position.ToString()}") - - return is_at_position - def check_entity_children_count(entity_id, expected_children_count): entity_children_count_matched_result = ( "Entity with a unique name found", @@ -47,19 +33,6 @@ def check_entity_children_count(entity_id, expected_children_count): return entity_children_count_matched -def get_children_ids_by_name(entity_id, entity_name): - entity = EditorEntity(entity_id) - children_entity_ids = entity.get_children_ids() - - result = [] - for child_entity_id in children_entity_ids: - child_entity = EditorEntity(child_entity_id) - child_entity_name = child_entity.get_name() - if child_entity_name == entity_name: - result.append(child_entity_id) - - return result - def open_base_tests_level(): helper.init_idle() helper.open_level("Prefab", "Base") diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp index d95a704e84..e9a5bee87a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp @@ -9,6 +9,8 @@ #include #include +#include +#include #include #include #include @@ -38,6 +40,13 @@ namespace AzToolsFramework AZ::Edit::SliceFlags::DontGatherReference); } } + + if (auto behaviorContext = azrtti_cast(context)) + { + behaviorContext->ConstantProperty( + "EditorPrefabComponentTypeId", BehaviorConstant(AZ::Uuid(EditorPrefabComponent::EditorPrefabComponentTypeId))) + ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation); + } } void EditorPrefabComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h index 8873787bd3..aa15b63ac2 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h @@ -16,7 +16,9 @@ namespace AzToolsFramework class EditorPrefabComponent : public AzToolsFramework::Components::EditorComponentBase { public: - AZ_COMPONENT(EditorPrefabComponent, "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}", EditorComponentBase); + static constexpr const char* const EditorPrefabComponentTypeId = "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}"; + + AZ_COMPONENT(EditorPrefabComponent, EditorPrefabComponentTypeId, EditorComponentBase); static void Reflect(AZ::ReflectContext* context); static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index ee34b628bb..d976c91c3e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -974,7 +974,7 @@ namespace AzToolsFramework return DeleteFromInstance(entityIds, true); } - PrefabOperationResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) + DuplicatePrefabResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) { if (entityIds.empty()) { @@ -1021,6 +1021,7 @@ namespace AzToolsFramework ScopedUndoBatch undoBatch("Duplicate Entities"); + EntityIdList duplicatedEntityAndInstanceIds; { AZ_PROFILE_SCOPE(AzToolsFramework, "DuplicateEntitiesInInstance::UndoCaptureAndDuplicateEntities"); @@ -1033,7 +1034,7 @@ namespace AzToolsFramework if (!retrieveEntitiesAndInstancesOutcome.IsSuccess()) { - return AZStd::move(retrieveEntitiesAndInstancesOutcome); + return AZ::Failure(retrieveEntitiesAndInstancesOutcome.TakeError()); } // Take a snapshot of the instance DOM before we manipulate it @@ -1044,8 +1045,6 @@ namespace AzToolsFramework PrefabDom instanceDomAfter; instanceDomAfter.CopyFrom(instanceDomBefore, instanceDomAfter.GetAllocator()); - EntityIdList duplicatedEntityAndInstanceIds; - // Duplicate any nested entities and instances as requested AZStd::unordered_map newInstanceAliasToOldInstanceMap; AZStd::unordered_map duplicateEntityAliasMap; @@ -1114,7 +1113,7 @@ namespace AzToolsFramework ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::SetSelectedEntities, duplicatedEntityAndInstanceIds); } - return AZ::Success(); + return AZ::Success(AZStd::move(duplicatedEntityAndInstanceIds)); } PrefabOperationResult PrefabPublicHandler::DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h index a9dadc3336..4961be9d77 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h @@ -63,7 +63,7 @@ namespace AzToolsFramework PrefabOperationResult DeleteEntitiesInInstance(const EntityIdList& entityIds) override; PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override; - PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; + DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h index 528d4f6d1b..ede857dd2b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h @@ -26,6 +26,7 @@ namespace AzToolsFramework { typedef AZ::Outcome CreatePrefabResult; typedef AZ::Outcome InstantiatePrefabResult; + typedef AZ::Outcome DuplicatePrefabResult; typedef AZ::Outcome PrefabOperationResult; typedef AZ::Outcome PrefabRequestResult; typedef AZ::Outcome PrefabEntityResult; @@ -160,14 +161,15 @@ namespace AzToolsFramework /** * Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance. * @param entities The entities to duplicate. - * @return An outcome object; on failure, it comes with an error message detailing the cause of the error. + * @return An outcome object with a list of ids of target entities' duplicates if duplication succeeded; + * on failure, it comes with an error message detailing the cause of the error. */ - virtual PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; + virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; /** * If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting * the container entity into a regular entity and putting it under the parent prefab, removing the link between this - * instance and the parent, removing links between this instance and it's nested instances, adding entities directly + * instance and the parent, removing links between this instance and its nested instances, and adding entities directly * owned by this instance under the parent instance. * Bails if the entity is not a container entity or belongs to the level prefab instance. * @param containerEntityId The container entity id of the instance to detach. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h index 7b23fffb7f..fd4b8a5f17 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h @@ -25,6 +25,7 @@ namespace AzToolsFramework { using CreatePrefabResult = AZ::Outcome; using InstantiatePrefabResult = AZ::Outcome; + using DuplicatePrefabResult = AZ::Outcome; using PrefabOperationResult = AZ::Outcome; /** @@ -69,6 +70,29 @@ namespace AzToolsFramework * Return an outcome object; on failure, it comes with an error message detailing the cause of the error. */ virtual PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) = 0; + + /** + * If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting + * the container entity into a regular entity and putting it under the parent prefab, removing the link between this + * instance and the parent, removing links between this instance and its nested instances, and adding entities directly + * owned by this instance under the parent instance. + * Bails if the entity is not a container entity or belongs to the level prefab instance. + * Return an outcome object; on failure, it comes with an error message detailing the cause of the error. + */ + virtual PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) = 0; + + /** + * Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance. + * Return an outcome object with a list of ids of given entities' duplicates if duplication succeeded; + * on failure, it comes with an error message detailing the cause of the error. + */ + virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; + + /** + * Get the file path to the prefab file for the prefab instance owning the entity provided. + * Returns the path to the prefab, or an empty path if the entity is owned by the level. + */ + virtual AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const = 0; }; using PrefabPublicRequestBus = AZ::EBus; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp index 0aaf81c4c9..3b69dcdfe4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp @@ -28,6 +28,9 @@ namespace AzToolsFramework ->Event("CreatePrefabInMemory", &PrefabPublicRequests::CreatePrefabInMemory) ->Event("InstantiatePrefab", &PrefabPublicRequests::InstantiatePrefab) ->Event("DeleteEntitiesAndAllDescendantsInInstance", &PrefabPublicRequests::DeleteEntitiesAndAllDescendantsInInstance) + ->Event("DetachPrefab", &PrefabPublicRequests::DetachPrefab) + ->Event("DuplicateEntitiesInInstance", &PrefabPublicRequests::DuplicateEntitiesInInstance) + ->Event("GetOwningInstancePrefabPath", &PrefabPublicRequests::GetOwningInstancePrefabPath) ; } } @@ -62,5 +65,19 @@ namespace AzToolsFramework return m_prefabPublicInterface->DeleteEntitiesAndAllDescendantsInInstance(entityIds); } + PrefabOperationResult PrefabPublicRequestHandler::DetachPrefab(const AZ::EntityId& containerEntityId) + { + return m_prefabPublicInterface->DetachPrefab(containerEntityId); + } + + DuplicatePrefabResult PrefabPublicRequestHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) + { + return m_prefabPublicInterface->DuplicateEntitiesInInstance(entityIds); + } + + AZStd::string PrefabPublicRequestHandler::GetOwningInstancePrefabPath(AZ::EntityId entityId) const + { + return m_prefabPublicInterface->GetOwningInstancePrefabPath(entityId).Native(); + } } // namespace Prefab } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h index ae0ed2a5d1..b24ea7ec2a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h @@ -34,6 +34,9 @@ namespace AzToolsFramework CreatePrefabResult CreatePrefabInMemory(const EntityIdList& entityIds, AZStd::string_view filePath) override; InstantiatePrefabResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override; PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override; + PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override; + DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; + AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const override; private: PrefabPublicInterface* m_prefabPublicInterface = nullptr; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp index 6c96209d56..1c2230fa83 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp @@ -23,7 +23,7 @@ namespace AzToolsFramework } //PrefabInstanceUndo - PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation) + PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation) : PrefabUndoBase(undoOperationName) { m_useImmediatePropagation = useImmediatePropagation; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h index 0946a36951..bc0b86a8c6 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h @@ -45,7 +45,7 @@ namespace AzToolsFramework : public PrefabUndoBase { public: - explicit PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation = true); + explicit PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation = true); void Capture( const PrefabDom& initialState, From 244878483a7fa04f6d7e10b06339f4e9669a6a7b Mon Sep 17 00:00:00 2001 From: Steve Pham <82231385+spham-amzn@users.noreply.github.com> Date: Thu, 21 Oct 2021 12:40:10 -0700 Subject: [PATCH 07/10] Update open file limit on linux for applications (#4878) * Programmatically update the ulimit for open files if the current limit is not enough Signed-off-by: Steve Pham --- .../Application/Application_Linux.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp index 158240210e..85669e093c 100644 --- a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp +++ b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp @@ -7,17 +7,35 @@ */ #include +#include #if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB #include #endif +constexpr rlim_t g_minimumOpenFileHandles = 65536L; + //////////////////////////////////////////////////////////////////////////////////////////////////// namespace AzFramework { //////////////////////////////////////////////////////////////////////////////////////////////// Application::Implementation* Application::Implementation::Create() { + // The default open file limit for processes may not be enough for O3DE applications. + // We will need to increase to the recommended value if the current open file limit + // is not sufficient. + rlimit currentLimit; + int get_limit_result = getrlimit(RLIMIT_NOFILE, ¤tLimit); + AZ_Warning("Application", get_limit_result == 0, "Unable to read current ulimit open file limits"); + if ((get_limit_result == 0) && (currentLimit.rlim_cur < g_minimumOpenFileHandles || currentLimit.rlim_max < g_minimumOpenFileHandles)) + { + rlimit newLimit; + newLimit.rlim_cur = g_minimumOpenFileHandles; // Soft Limit + newLimit.rlim_max = g_minimumOpenFileHandles; // Hard Limit + [[maybe_unused]] int set_limit_result = setrlimit(RLIMIT_NOFILE, &newLimit); + AZ_Assert(set_limit_result == 0, "Unable to update open file limits"); + } + #if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB return aznew XcbApplication(); #elif PAL_TRAIT_LINUX_WINDOW_MANAGER_WAYLAND From 50f66bde69c00e507df4861e0eb8aa934b0f8923 Mon Sep 17 00:00:00 2001 From: amzn-phist <52085794+amzn-phist@users.noreply.github.com> Date: Thu, 21 Oct 2021 15:38:56 -0500 Subject: [PATCH 08/10] Remove the use of a .Stub version of the Wwise Gem (#4879) * Remove the definition of a 'Stub' Wwise Gem This removes the conditional compilation of a '.Stub' version of the Wwise Gem. Now, all we do is not define any of the AudioEngineWwise targets in CMake whenever Wwise is not available (SDK not found or platform unsupported). Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> * Remove the remaining stub files, no longer needed This removes the files.cmake and stub module cpp files that made up the .Stub version of this Gem. These are no longer needed. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com> --- Gems/AudioEngineWwise/Code/CMakeLists.txt | 27 +++++-------------- .../Code/Platform/Android/PAL_android.cmake | 2 +- .../Code/Platform/Linux/PAL_linux.cmake | 2 +- .../Code/Platform/Mac/PAL_mac.cmake | 2 +- .../Code/Platform/Windows/PAL_windows.cmake | 2 +- .../Code/Platform/iOS/PAL_ios.cmake | 2 +- .../Source/AudioEngineWwiseModule_Stub.cpp | 11 -------- .../Code/audioenginewwise_stub_files.cmake | 11 -------- 8 files changed, 12 insertions(+), 47 deletions(-) delete mode 100644 Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp delete mode 100644 Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake diff --git a/Gems/AudioEngineWwise/Code/CMakeLists.txt b/Gems/AudioEngineWwise/Code/CMakeLists.txt index 98a31fb91a..33c80bc31c 100644 --- a/Gems/AudioEngineWwise/Code/CMakeLists.txt +++ b/Gems/AudioEngineWwise/Code/CMakeLists.txt @@ -18,31 +18,18 @@ set(AUDIOENGINEWWISE_COMPILEDEFINITIONS find_package(Wwise MODULE) ################################################################################ -# Server / Unsupported +# Servers +# (and situations where Wwise SDK is not found or otherwise unavailable) ################################################################################ -if (PAL_TRAIT_BUILD_SERVER_SUPPORTED OR PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB OR NOT Wwise_FOUND) - # Stub gem for server and unsupported platforms. Audio Engine Wwise is client only - ly_add_target( - NAME AudioEngineWwise.Stub ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} - NAMESPACE Gem - FILES_CMAKE - audioenginewwise_stub_files.cmake - BUILD_DEPENDENCIES - PRIVATE - AZ::AzCore - ) -endif() - -if (PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB OR NOT Wwise_FOUND) - # setup aliases so stubs will be used if something references AudioEngineWwise(.Editor) - add_library(Gem::AudioEngineWwise ALIAS AudioEngineWwise.Stub) - add_library(Gem::AudioEngineWwise.Editor ALIAS AudioEngineWwise.Stub) +if(NOT PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED OR NOT Wwise_FOUND) + # Don't create any Gem targets and aliases. Nothing should depend on this + # Gem directly, because if it doesn't define targets it will cause an error. return() endif() ################################################################################ -# Runtime / Game +# Clients ################################################################################ ly_add_target( NAME AudioEngineWwise.Static STATIC @@ -181,7 +168,7 @@ if (PAL_TRAIT_BUILD_TESTS_SUPPORTED) endif() ################################################################################ -# Tools / Editor +# Tools / Builders ################################################################################ if (PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_target( diff --git a/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake b/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake b/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake b/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake b/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake b/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp b/Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp deleted file mode 100644 index 4344eb6072..0000000000 --- a/Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp +++ /dev/null @@ -1,11 +0,0 @@ -/* - * 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 - -AZ_DECLARE_MODULE_CLASS(Gem_AudioEngineWwise, AZ::Module) diff --git a/Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake b/Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake deleted file mode 100644 index 5597c28d04..0000000000 --- a/Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake +++ /dev/null @@ -1,11 +0,0 @@ -# -# 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 - Source/AudioEngineWwiseModule_Stub.cpp -) From 52112be1acaa4c10c5dbc3e7e2a0f5d87ea34148 Mon Sep 17 00:00:00 2001 From: Danilo Aimini <82231674+AMZN-daimini@users.noreply.github.com> Date: Thu, 21 Oct 2021 13:43:21 -0700 Subject: [PATCH 09/10] LYN-7448 + LYN-7542 + LYN-7543 | Focus Mode - UX Improvements (#4837) * Introduce Outliner button to simplify Prefab editing Signed-off-by: Danilo Aimini <82231674+AMZN-daimini@users.noreply.github.com> * Fix Focus Mode and disabled entities colors Signed-off-by: Danilo Aimini <82231674+AMZN-daimini@users.noreply.github.com> * Fix - propagate the event if the OnOutlinerItemClick function returns false. This does not change current behavior but makes more sense in the context of future handlers. Signed-off-by: Danilo Aimini <82231674+AMZN-daimini@users.noreply.github.com> * Adjust disabled colors to match UX recommendations. Signed-off-by: Danilo Aimini <82231674+AMZN-daimini@users.noreply.github.com> * Display the edit button even if the prefab is disabled. Remove prefabWip check (it will be removed for focus mode by the time this goes in). Default to disabled capsule color for borders to save on checks. Signed-off-by: Danilo Aimini <82231674+AMZN-daimini@users.noreply.github.com> * Disable edit button on disabled prefabs as it caused conflicts in nested prefabs. May explore that possibility later. Signed-off-by: Danilo Aimini <82231674+AMZN-daimini@users.noreply.github.com> * Change disabled text color to be darker, as asked by UX. Signed-off-by: Danilo Aimini <82231674+AMZN-daimini@users.noreply.github.com> --- .../Images/Entity/prefab_edit_close.svg | 4 + .../Images/Entity/prefab_edit_open.svg | 4 + .../AzQtComponents/Images/resources.qrc | 2 + .../Prefab/PrefabFocusHandler.cpp | 28 ++++ .../Prefab/PrefabFocusHandler.h | 1 + .../Prefab/PrefabFocusPublicInterface.h | 7 +- .../EditorEntityUiHandlerBase.cpp | 19 ++- .../EditorEntityUiHandlerBase.h | 13 +- .../UI/Outliner/EntityOutlinerListModel.cpp | 28 +++- .../UI/Outliner/EntityOutlinerTreeView.cpp | 7 + .../UI/Outliner/EntityOutlinerTreeView.hxx | 2 + .../UI/Outliner/EntityOutlinerWidget.cpp | 21 ++- .../UI/Prefab/PrefabUiHandler.cpp | 131 +++++++++++++++++- .../UI/Prefab/PrefabUiHandler.h | 11 +- .../ViewportUi/ViewportUiDisplay.cpp | 2 +- 15 files changed, 265 insertions(+), 15 deletions(-) create mode 100644 Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_close.svg create mode 100644 Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_open.svg diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_close.svg b/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_close.svg new file mode 100644 index 0000000000..3fccf0e716 --- /dev/null +++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_open.svg b/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_open.svg new file mode 100644 index 0000000000..55704ec230 --- /dev/null +++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc b/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc index 0c8fedc79d..15049aeb69 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc +++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc @@ -6,6 +6,8 @@ Entity/layer.svg Entity/prefab.svg Entity/prefab_edit.svg + Entity/prefab_edit_open.svg + Entity/prefab_edit_close.svg Level/level.svg diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp index 09b5745a90..8bc258fef9 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp @@ -206,6 +206,34 @@ namespace AzToolsFramework::Prefab return instance.has_value() && (&instance->get() == &m_focusedInstance->get()); } + bool PrefabFocusHandler::IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const + { + if (!m_focusedInstance.has_value()) + { + // PrefabFocusHandler has not been initialized yet. + return false; + } + + if (!entityId.IsValid()) + { + return false; + } + + InstanceOptionalReference instance = m_instanceEntityMapperInterface->FindOwningInstance(entityId); + + while (instance.has_value()) + { + if (&instance->get() == &m_focusedInstance->get()) + { + return true; + } + + instance = instance->get().GetParentInstance(); + } + + return false; + } + const AZ::IO::Path& PrefabFocusHandler::GetPrefabFocusPath([[maybe_unused]] AzFramework::EntityContextId entityContextId) const { return m_instanceFocusPath; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h index 6a71365d8e..cb8165e7f0 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h @@ -53,6 +53,7 @@ namespace AzToolsFramework::Prefab PrefabFocusOperationResult FocusOnPathIndex(AzFramework::EntityContextId entityContextId, int index) override; AZ::EntityId GetFocusedPrefabContainerEntityId(AzFramework::EntityContextId entityContextId) const override; bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const override; + bool IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const override; const AZ::IO::Path& GetPrefabFocusPath(AzFramework::EntityContextId entityContextId) const override; const int GetPrefabFocusPathLength(AzFramework::EntityContextId entityContextId) const override; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusPublicInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusPublicInterface.h index 86e476b56f..5bd4c6b0f6 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusPublicInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusPublicInterface.h @@ -37,10 +37,15 @@ namespace AzToolsFramework::Prefab //! Returns the entity id of the container entity for the instance the prefab system is focusing on. virtual AZ::EntityId GetFocusedPrefabContainerEntityId(AzFramework::EntityContextId entityContextId) const = 0; + //! Returns whether the entity belongs to the instance that is being focused on. + //! @param entityId The entityId of the queried entity. + //! @return true if the entity belongs to the focused instance, false otherwise. + virtual bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const = 0; + //! Returns whether the entity belongs to the instance that is being focused on, or one of its descendants. //! @param entityId The entityId of the queried entity. //! @return true if the entity belongs to the focused instance or one of its descendants, false otherwise. - virtual bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const = 0; + virtual bool IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const = 0; //! Returns the path from the root instance to the currently focused instance. //! @return A path composed from the names of the container entities for the instance path. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.cpp index 3cdcade1b0..866080a83f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.cpp @@ -101,8 +101,25 @@ namespace AzToolsFramework { } - void EditorEntityUiHandlerBase::OnDoubleClick([[maybe_unused]] AZ::EntityId entityId) const + bool EditorEntityUiHandlerBase::OnOutlinerItemClick( + [[maybe_unused]] const QPoint& position, + [[maybe_unused]] const QStyleOptionViewItem& option, + [[maybe_unused]] const QModelIndex& index) const + { + return false; + } + + void EditorEntityUiHandlerBase::OnOutlinerItemExpand([[maybe_unused]] const QModelIndex& index) const + { + } + + void EditorEntityUiHandlerBase::OnOutlinerItemCollapse([[maybe_unused]] const QModelIndex& index) const { } + bool EditorEntityUiHandlerBase::OnEntityDoubleClick([[maybe_unused]] AZ::EntityId entityId) const + { + return false; + } + } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.h index 9554ad01fc..96d393efa1 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.h @@ -61,8 +61,17 @@ namespace AzToolsFramework virtual void PaintDescendantForeground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index, const QModelIndex& descendantIndex) const; - //! Triggered when the entity is double clicked in the Outliner. - virtual void OnDoubleClick(AZ::EntityId entityId) const; + //! Triggered when the entity is clicked in the Outliner. + //! @return True if the click has been handled and should not be propagated, false otherwise. + virtual bool OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const; + //! Triggered when an entity's children are expanded in the Outliner. + virtual void OnOutlinerItemExpand(const QModelIndex& index) const; + //! Triggered when an entity's children are collapsed in the Outliner. + virtual void OnOutlinerItemCollapse(const QModelIndex& index) const; + + //! Triggered when the entity is double clicked in the Outliner or in the Viewport. + //! @return True if the double click has been handled and should not be propagated, false otherwise. + virtual bool OnEntityDoubleClick(AZ::EntityId entityId) const; private: EditorEntityUiHandlerId m_handlerId = 0; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp index 3f8023c1e3..a5f1e29942 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp @@ -11,10 +11,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -2287,7 +2289,14 @@ namespace AzToolsFramework // Now we setup a Text Document so it can draw the rich text QTextDocument textDoc; textDoc.setDefaultFont(optionV4.font); - textDoc.setDefaultStyleSheet("body {color: white}"); + if (option.state & QStyle::State_Enabled) + { + textDoc.setDefaultStyleSheet("body {color: white}"); + } + else + { + textDoc.setDefaultStyleSheet("body {color: #7C7C7C}"); + } textDoc.setHtml("" + entityNameRichText + ""); painter->translate(textRect.topLeft()); textDoc.setTextWidth(textRect.width()); @@ -2326,6 +2335,23 @@ namespace AzToolsFramework return true; } + if (event->type() == QEvent::MouseButtonPress) + { + AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); + + if (auto editorEntityUiInterface = AZ::Interface::Get(); editorEntityUiInterface != nullptr) + { + auto mouseEvent = static_cast(event); + + auto entityUiHandler = editorEntityUiInterface->GetHandler(entityId); + + if (entityUiHandler && entityUiHandler->OnOutlinerItemClick(mouseEvent->pos(), option, index)) + { + return true; + } + } + } + return QStyledItemDelegate::editorEvent(event, model, option, index); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp index d3138f5139..d94ced392c 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp @@ -73,6 +73,8 @@ namespace AzToolsFramework void EntityOutlinerTreeView::leaveEvent([[maybe_unused]] QEvent* event) { m_mousePosition = QPoint(); + m_currentHoveredIndex = QModelIndex(); + update(); } void EntityOutlinerTreeView::mousePressEvent(QMouseEvent* event) @@ -129,6 +131,11 @@ namespace AzToolsFramework } m_mousePosition = event->pos(); + if (QModelIndex hoveredIndex = indexAt(m_mousePosition); m_currentHoveredIndex != indexAt(m_mousePosition)) + { + m_currentHoveredIndex = hoveredIndex; + update(); + } //process mouse movement as normal, potentially triggering drag and drop QTreeView::mouseMoveEvent(event); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx index 5d76ec6db2..42b42a59b4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx @@ -90,6 +90,8 @@ namespace AzToolsFramework const QColor m_selectedColor = QColor(255, 255, 255, 45); const QColor m_hoverColor = QColor(255, 255, 255, 30); + QModelIndex m_currentHoveredIndex; + EditorEntityUiInterface* m_editorEntityFrameworkInterface; }; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp index 4d53102edb..3e97f967fc 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp @@ -902,6 +902,7 @@ namespace AzToolsFramework EditorPickModeRequestBus::Broadcast( &EditorPickModeRequests::StopEntityPickMode); + return; } switch (index.column()) @@ -918,18 +919,30 @@ namespace AzToolsFramework { if (AZ::EntityId entityId = GetEntityIdFromIndex(index); auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId)) { - entityUiHandler->OnDoubleClick(entityId); + entityUiHandler->OnEntityDoubleClick(entityId); } } void EntityOutlinerWidget::OnTreeItemExpanded(const QModelIndex& index) { - m_listModel->OnEntityExpanded(GetEntityIdFromIndex(index)); + AZ::EntityId entityId = GetEntityIdFromIndex(index); + if (auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId)) + { + entityUiHandler->OnOutlinerItemExpand(index); + } + + m_listModel->OnEntityExpanded(entityId); } void EntityOutlinerWidget::OnTreeItemCollapsed(const QModelIndex& index) { - m_listModel->OnEntityCollapsed(GetEntityIdFromIndex(index)); + AZ::EntityId entityId = GetEntityIdFromIndex(index); + if (auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId)) + { + entityUiHandler->OnOutlinerItemCollapse(index); + } + + m_listModel->OnEntityCollapsed(entityId); } void EntityOutlinerWidget::OnExpandEntity(const AZ::EntityId& entityId, bool expand) @@ -1163,7 +1176,7 @@ namespace AzToolsFramework { QTimer::singleShot(1, this, [this]() { m_gui->m_objectTree->setUpdatesEnabled(true); - m_gui->m_objectTree->expandToDepth(0); + m_gui->m_objectTree->expand(m_proxyModel->index(0,0)); }); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp index 7c2c65d204..447f94fc15 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp @@ -21,10 +21,16 @@ namespace AzToolsFramework { + const QColor PrefabUiHandler::m_backgroundColor = QColor("#444444"); + const QColor PrefabUiHandler::m_backgroundHoverColor = QColor("#5A5A5A"); + const QColor PrefabUiHandler::m_backgroundSelectedColor = QColor("#656565"); const QColor PrefabUiHandler::m_prefabCapsuleColor = QColor("#1E252F"); + const QColor PrefabUiHandler::m_prefabCapsuleDisabledColor = QColor("#35383C"); const QColor PrefabUiHandler::m_prefabCapsuleEditColor = QColor("#4A90E2"); const QString PrefabUiHandler::m_prefabIconPath = QString(":/Entity/prefab.svg"); const QString PrefabUiHandler::m_prefabEditIconPath = QString(":/Entity/prefab_edit.svg"); + const QString PrefabUiHandler::m_prefabEditOpenIconPath = QString(":/Entity/prefab_edit_open.svg"); + const QString PrefabUiHandler::m_prefabEditCloseIconPath = QString(":/Entity/prefab_edit_close.svg"); PrefabUiHandler::PrefabUiHandler() { @@ -75,7 +81,7 @@ namespace AzToolsFramework if (!path.empty()) { - tooltip = QObject::tr("%1").arg(path.Native().data()); + tooltip = QObject::tr("Double click to edit.\n%1").arg(path.Native().data()); } return tooltip; @@ -102,13 +108,20 @@ namespace AzToolsFramework AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); const bool isFirstColumn = index.column() == EntityOutlinerListModel::ColumnName; const bool isLastColumn = index.column() == EntityOutlinerListModel::ColumnLockToggle; - const bool hasVisibleChildren = index.data(EntityOutlinerListModel::ExpandedRole).value() && index.model()->hasChildren(index); + QModelIndex firstColumnIndex = index.siblingAtColumn(EntityOutlinerListModel::ColumnName); + const bool hasVisibleChildren = + firstColumnIndex.data(EntityOutlinerListModel::ExpandedRole).value() && + firstColumnIndex.model()->hasChildren(firstColumnIndex); QColor backgroundColor = m_prefabCapsuleColor; if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) { backgroundColor = m_prefabCapsuleEditColor; } + else if (!(option.state & QStyle::State_Enabled)) + { + backgroundColor = m_prefabCapsuleDisabledColor; + } QPainterPath backgroundPath; backgroundPath.setFillRule(Qt::WindingFill); @@ -184,7 +197,8 @@ namespace AzToolsFramework const bool isFirstColumn = descendantIndex.column() == EntityOutlinerListModel::ColumnName; const bool isLastColumn = descendantIndex.column() == EntityOutlinerListModel::ColumnLockToggle; - QColor borderColor = m_prefabCapsuleColor; + // There is no legal way of opening prefabs in their default state, so default to disabled. + QColor borderColor = m_prefabCapsuleDisabledColor; if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) { borderColor = m_prefabCapsuleEditColor; @@ -273,6 +287,71 @@ namespace AzToolsFramework painter->restore(); } + void PrefabUiHandler::PaintItemForeground(QPainter* painter, const QStyleOptionViewItem& option, [[maybe_unused]] const QModelIndex& index) const + { + AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); + const QPoint offset = QPoint(-18, 3); + QModelIndex firstColumnIndex = index.siblingAtColumn(EntityOutlinerListModel::ColumnName); + const int iconSize = 16; + const bool isHovered = (option.state & QStyle::State_MouseOver); + const bool isSelected = index.data(EntityOutlinerListModel::SelectedRole).template value(); + const bool isFirstColumn = index.column() == EntityOutlinerListModel::ColumnName; + const bool isExpanded = + firstColumnIndex.data(EntityOutlinerListModel::ExpandedRole).value() && + firstColumnIndex.model()->hasChildren(firstColumnIndex); + + if (!isFirstColumn || !(option.state & QStyle::State_Enabled)) + { + return; + } + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + + if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) + { + // Only show the close icon if the prefab is expanded. + // This allows the prefab container to be opened if it was collapsed during propagation. + if (!isExpanded) + { + return; + } + + // Use the same color as the background. + QColor backgroundColor = m_backgroundColor; + if (isSelected) + { + backgroundColor = m_backgroundSelectedColor; + } + else if (isHovered) + { + backgroundColor = m_backgroundHoverColor; + } + + // Paint a rect to cover up the expander. + QRect rect = QRect(0, 0, 16, 16); + rect.translate(option.rect.topLeft() + offset); + painter->fillRect(rect, backgroundColor); + + // Paint the icon. + QIcon closeIcon = QIcon(m_prefabEditCloseIconPath); + painter->drawPixmap(option.rect.topLeft() + offset, closeIcon.pixmap(iconSize)); + } + else + { + // Only show the edit icon on hover. + if (!isHovered) + { + return; + } + + QIcon openIcon = QIcon(m_prefabEditOpenIconPath); + painter->drawPixmap(option.rect.topLeft() + offset, openIcon.pixmap(iconSize)); + } + + painter->restore(); + } + bool PrefabUiHandler::IsLastVisibleChild(const QModelIndex& parent, const QModelIndex& child) { QModelIndex lastVisibleItemIndex = GetLastVisibleChild(parent); @@ -314,9 +393,53 @@ namespace AzToolsFramework return Internal_GetLastVisibleChild(model, lastChild); } - void PrefabUiHandler::OnDoubleClick(AZ::EntityId entityId) const + bool PrefabUiHandler::OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const + { + AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); + const QPoint offset = QPoint(-18, 3); + + if (m_prefabFocusPublicInterface->IsOwningPrefabInFocusHierarchy(entityId)) + { + QRect iconRect = QRect(0, 0, 16, 16); + iconRect.translate(option.rect.topLeft() + offset); + + if (iconRect.contains(position)) + { + if (!m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) + { + // Focus on this prefab. + m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId); + } + + // Don't propagate event. + return true; + } + } + + return false; + } + + void PrefabUiHandler::OnOutlinerItemCollapse(const QModelIndex& index) const + { + AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); + + if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) + { + auto editorEntityContextId = AzFramework::EntityContextId::CreateNull(); + EditorEntityContextRequestBus::BroadcastResult(editorEntityContextId, &EditorEntityContextRequests::GetEditorEntityContextId); + + // Go one level up. + int length = m_prefabFocusPublicInterface->GetPrefabFocusPathLength(editorEntityContextId); + m_prefabFocusPublicInterface->FocusOnPathIndex(editorEntityContextId, length - 2); + } + } + + bool PrefabUiHandler::OnEntityDoubleClick(AZ::EntityId entityId) const { // Focus on this prefab m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId); + + // Don't propagate event. + return true; } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h index 7c68d9fd95..6c78afc5b7 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h @@ -36,7 +36,10 @@ namespace AzToolsFramework void PaintItemBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; void PaintDescendantBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index, const QModelIndex& descendantIndex) const override; - void OnDoubleClick(AZ::EntityId entityId) const override; + void PaintItemForeground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + bool OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + void OnOutlinerItemCollapse(const QModelIndex& index) const override; + bool OnEntityDoubleClick(AZ::EntityId entityId) const override; private: Prefab::PrefabFocusPublicInterface* m_prefabFocusPublicInterface = nullptr; @@ -48,9 +51,15 @@ namespace AzToolsFramework static constexpr int m_prefabCapsuleRadius = 6; static constexpr int m_prefabBorderThickness = 2; + static const QColor m_backgroundColor; + static const QColor m_backgroundHoverColor; + static const QColor m_backgroundSelectedColor; static const QColor m_prefabCapsuleColor; + static const QColor m_prefabCapsuleDisabledColor; static const QColor m_prefabCapsuleEditColor; static const QString m_prefabIconPath; static const QString m_prefabEditIconPath; + static const QString m_prefabEditOpenIconPath; + static const QString m_prefabEditCloseIconPath; }; } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp index a289d914d6..43e2a6793f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp @@ -20,7 +20,7 @@ namespace AzToolsFramework::ViewportUi::Internal { const static int HighlightBorderSize = 5; const static int TopHighlightBorderSize = 25; - const static char* HighlightBorderColor = "#44B2F8"; + const static char* HighlightBorderColor = "#4A90E2"; static void UnparentWidgets(ViewportUiElementIdInfoLookup& viewportUiElementIdInfoLookup) { From ea442b80d1e8725269dc41bb039c0ad8dbb6a676 Mon Sep 17 00:00:00 2001 From: AMZN-AlexOteiza <82234181+AMZN-AlexOteiza@users.noreply.github.com> Date: Thu, 21 Oct 2021 22:03:18 +0100 Subject: [PATCH 10/10] Renamed ctest_pytest.ini to pytest.ini so it is used by default, added TestSuite_ as collection file (#4822) * Fixed warnings of unused marks, renamed ctest_pytest.ini to pytest.ini to better consistency on runs * Fixed some test suites to run propertly * Fix missing arguments * Fixed missing cmakelists and renamed missing file * Temp disable editor_testing_tests as timeout in jenkins --- .../Gem/PythonTests/Atom/TestSuite_Sandbox.py | 4 ---- .../Gem/PythonTests/CMakeLists.txt | 3 +++ .../PythonTests/Physics/TestSuite_Utils.py | 21 +++++++--------- .../editor_test_testing/CMakeLists.txt | 24 +++++++++++++++++++ .../editor_test_testing/TestSuite_Main.py | 9 +++---- .../editor_test_testing/conftest.py | 8 ------- Tools/LyTestTools/setup.py | 3 ++- cmake/LYTestWrappers.cmake | 2 +- cmake/Platform/Common/Install_common.cmake | 2 +- ctest_pytest.ini => pytest.ini | 3 ++- 10 files changed, 47 insertions(+), 32 deletions(-) create mode 100644 AutomatedTesting/Gem/PythonTests/editor_test_testing/CMakeLists.txt delete mode 100644 AutomatedTesting/Gem/PythonTests/editor_test_testing/conftest.py rename ctest_pytest.ini => pytest.ini (91%) diff --git a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py index 9bb7f9c50e..ad45e51080 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py @@ -14,10 +14,6 @@ import editor_python_test_tools.hydra_test_utils as hydra logger = logging.getLogger(__name__) TEST_DIRECTORY = os.path.join(os.path.dirname(__file__), "tests") - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -@pytest.mark.parametrize("level", ["auto_test"]) class TestAtomEditorComponentsSandbox(object): # It requires at least one test diff --git a/AutomatedTesting/Gem/PythonTests/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/CMakeLists.txt index 2ca7f7ea2b..fd3222ba83 100644 --- a/AutomatedTesting/Gem/PythonTests/CMakeLists.txt +++ b/AutomatedTesting/Gem/PythonTests/CMakeLists.txt @@ -58,3 +58,6 @@ add_subdirectory(smoke) ## AWS ## add_subdirectory(AWS) + +## Integration tests for editor testing framework ## +add_subdirectory(editor_test_testing) diff --git a/AutomatedTesting/Gem/PythonTests/Physics/TestSuite_Utils.py b/AutomatedTesting/Gem/PythonTests/Physics/TestSuite_Utils.py index a3fd5c741f..61c2816f50 100755 --- a/AutomatedTesting/Gem/PythonTests/Physics/TestSuite_Utils.py +++ b/AutomatedTesting/Gem/PythonTests/Physics/TestSuite_Utils.py @@ -9,20 +9,17 @@ import pytest import sys import ly_test_tools.environment.file_system as fs -from .FileManagement import FileManagement as fm +from .utils.FileManagement import FileManagement as fm sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../automatedtesting_shared') -from .base import TestAutomationBase +from base import TestAutomationBase - -@pytest.mark.parametrize("platform", ["win_x64_vs2017"]) -@pytest.mark.parametrize("configuration", ["profile"]) -@pytest.mark.parametrize("spec", ["all"]) +@pytest.mark.parametrize("launcher_platform", ['windows_editor']) @pytest.mark.parametrize("project", ["AutomatedTesting"]) class TestUtils(TestAutomationBase): @fm.file_revert("UtilTest_Physmaterial_Editor_TestLibrary.physmaterial", r"AutomatedTesting\Levels\Physics\Physmaterial_Editor_Test") - def test_physmaterial_editor(self, request, workspace, editor): + def test_physmaterial_editor(self, request, workspace, launcher_platform, editor): """ Tests functionality of physmaterial editing utility :param workspace: Fixture containing platform and project detail @@ -35,11 +32,11 @@ class TestUtils(TestAutomationBase): unexpected_lines = ["Assert"] self._run_test(request, workspace, editor, physmaterial_editor_test_module, expected_lines, unexpected_lines) - def test_UtilTest_Tracer_PicksErrorsAndWarnings(self, request, workspace, editor): + def test_UtilTest_Tracer_PicksErrorsAndWarnings(self, request, workspace, launcher_platform, editor): from .utils import UtilTest_Tracer_PicksErrorsAndWarnings as testcase_module self._run_test(request, workspace, editor, testcase_module, [], []) - def test_FileManagement_FindingFiles(self, workspace): + def test_FileManagement_FindingFiles(self, workspace, launcher_platform): """ Tests the functionality of "searching for files" with FileManagement._find_files() :param workspace: ly_test_tools workspace fixture @@ -110,7 +107,7 @@ class TestUtils(TestAutomationBase): find_me_too_path, found_me["FindMeToo.txt"] ) - def test_FileManagement_FileBackup(self, workspace): + def test_FileManagement_FileBackup(self, workspace, launcher_platform): """ Tests the functionality of the file back up system via the FileManagement class :param workspace: ly_test_tools workspace fixture @@ -167,7 +164,7 @@ class TestUtils(TestAutomationBase): del file_map[target_file_path] fm._save_file_map(file_map) - def test_FileManagement_FileRestoration(self, workspace): + def test_FileManagement_FileRestoration(self, workspace, launcher_platform): """ Tests the restore file system via the FileManagement class :param workspace: ly_test_tools workspace fixture @@ -261,7 +258,7 @@ class TestUtils(TestAutomationBase): ["FindMe.txt", "FindMeToo.txt"], parent_path=r"AutomatedTesting\levels\Utils\Managed_files", search_subdirs=True ) @fm.file_override("default.physxconfiguration", "UtilTest_PhysxConfig_Override.physxconfiguration") - def test_UtilTest_Managed_Files(self, request, workspace, editor): + def test_UtilTest_Managed_Files(self, request, workspace, editor, launcher_platform): from .utils import UtilTest_Managed_Files as test_module expected_lines = [] diff --git a/AutomatedTesting/Gem/PythonTests/editor_test_testing/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/editor_test_testing/CMakeLists.txt new file mode 100644 index 0000000000..dbf26d66cd --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/editor_test_testing/CMakeLists.txt @@ -0,0 +1,24 @@ +# +# 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 +# +# + +# This timeouts on jenkins, investigation is needed. Commment for now +# +#if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS) +# ly_add_pytest( +# NAME AutomatedTesting::EditorTestTesting +# TEST_SUITE main +# TEST_SERIAL +# PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main.py +# RUNTIME_DEPENDENCIES +# Legacy::Editor +# AZ::AssetProcessor +# AutomatedTesting.Assets +# COMPONENT +# TestTools +# ) +#endif() diff --git a/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py index 7c7e063951..15ba6690a1 100644 --- a/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py @@ -35,10 +35,11 @@ class TestEditorTest: @classmethod def setup_class(cls): TestEditorTest.args = sys.argv.copy() - build_dir_arg_index = TestEditorTest.args.index("--build-directory") - if build_dir_arg_index < 0: - print("Error: Must pass --build-directory argument in order to run this test") - sys.exit(-2) + build_dir_arg_index = -1 + try: + build_dir_arg_index = TestEditorTest.args.index("--build-directory") + except ValueError as ex: + raise ValueError("Must pass --build-directory argument in order to run this test") TestEditorTest.args[build_dir_arg_index+1] = os.path.abspath(TestEditorTest.args[build_dir_arg_index+1]) TestEditorTest.args.append("-s") diff --git a/AutomatedTesting/Gem/PythonTests/editor_test_testing/conftest.py b/AutomatedTesting/Gem/PythonTests/editor_test_testing/conftest.py deleted file mode 100644 index 1f49f7111b..0000000000 --- a/AutomatedTesting/Gem/PythonTests/editor_test_testing/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -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 -""" - -pytest_plugins = ["pytester"] diff --git a/Tools/LyTestTools/setup.py b/Tools/LyTestTools/setup.py index e23f914dab..a523cbc2dd 100755 --- a/Tools/LyTestTools/setup.py +++ b/Tools/LyTestTools/setup.py @@ -48,7 +48,8 @@ if __name__ == '__main__': 'ly_test_tools=ly_test_tools._internal.pytest_plugin.test_tools_fixtures', 'testrail_filter=ly_test_tools._internal.pytest_plugin.case_id', 'terminal_report=ly_test_tools._internal.pytest_plugin.terminal_report', - 'editor_test=ly_test_tools._internal.pytest_plugin.editor_test' + 'editor_test=ly_test_tools._internal.pytest_plugin.editor_test', + 'pytester=_pytest.pytester' ], }, ) diff --git a/cmake/LYTestWrappers.cmake b/cmake/LYTestWrappers.cmake index 87649c5be6..9b139645fb 100644 --- a/cmake/LYTestWrappers.cmake +++ b/cmake/LYTestWrappers.cmake @@ -17,7 +17,7 @@ set(LY_GOOGLETEST_EXTRA_PARAMS CACHE STRING "Allows injection of additional opti find_package(Python REQUIRED MODULE) -ly_set(LY_PYTEST_EXECUTABLE ${LY_PYTHON_CMD} -B -m pytest -v --tb=short --show-capture=log -c ${LY_ROOT_FOLDER}/ctest_pytest.ini --build-directory "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$") +ly_set(LY_PYTEST_EXECUTABLE ${LY_PYTHON_CMD} -B -m pytest -v --tb=short --show-capture=log -c ${LY_ROOT_FOLDER}/pytest.ini --build-directory "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$") ly_set(LY_TEST_GLOBAL_KNOWN_SUITE_NAMES "smoke" "main" "periodic" "benchmark" "sandbox" "awsi") ly_set(LY_TEST_GLOBAL_KNOWN_REQUIREMENTS "gpu") diff --git a/cmake/Platform/Common/Install_common.cmake b/cmake/Platform/Common/Install_common.cmake index c453fadd2e..44cdeb1994 100644 --- a/cmake/Platform/Common/Install_common.cmake +++ b/cmake/Platform/Common/Install_common.cmake @@ -364,7 +364,7 @@ function(ly_setup_o3de_install) # Misc install(FILES - ${LY_ROOT_FOLDER}/ctest_pytest.ini + ${LY_ROOT_FOLDER}/pytest.ini ${LY_ROOT_FOLDER}/LICENSE.txt ${LY_ROOT_FOLDER}/README.md DESTINATION . diff --git a/ctest_pytest.ini b/pytest.ini similarity index 91% rename from ctest_pytest.ini rename to pytest.ini index 93310a522d..65c93e0eb2 100644 --- a/ctest_pytest.ini +++ b/pytest.ini @@ -7,10 +7,11 @@ # [pytest] -python_files = 'test_*.py' , '*_test.py' , '*_tests.py' +python_files = 'test_*.py' , '*_test.py' , '*_tests.py', 'TestSuite_*.py' norecursedirs = Python/2.7.* Python/3.7.5 BinTemp Cache SDKs JenkinsScripts cmake junit_family=legacy log_format=%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) +addopts='--tb=short' '--show-capture=log' # primary suite markers which should appear on every filterable test and be mutually exclusive: markers = SUITE_smoke: Tiny, quick tests of fundamental operation (tests with no suite marker will also execute here in CI)