Index: ps/trunk/source/graphics/Model.cpp =================================================================== --- ps/trunk/source/graphics/Model.cpp (revision 27879) +++ ps/trunk/source/graphics/Model.cpp (revision 27880) @@ -1,538 +1,538 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "Model.h" #include "graphics/Decal.h" #include "graphics/MeshManager.h" #include "graphics/ModelDef.h" #include "graphics/ObjectEntry.h" #include "graphics/SkeletonAnim.h" #include "graphics/SkeletonAnimDef.h" #include "maths/BoundingBoxAligned.h" #include "maths/Quaternion.h" #include "lib/sysdep/rtl.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Profile.h" #include "renderer/RenderingOptions.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/Simulation2.h" CModel::CModel(const CSimulation2& simulation, const CMaterial& material, const CModelDefPtr& modeldef) : m_Simulation{simulation}, m_Material{material}, m_pModelDef{modeldef} { const size_t numberOfBones = modeldef->GetNumBones(); if (numberOfBones != 0) { const size_t numberOfBlends = modeldef->GetNumBlends(); // allocate matrices for bone transformations // (one extra matrix is used for the special case of bind-shape relative weighting) m_BoneMatrices = (CMatrix3D*)rtl_AllocateAligned(sizeof(CMatrix3D) * (numberOfBones + 1 + numberOfBlends), 16); for (size_t i = 0; i < numberOfBones + 1 + numberOfBlends; ++i) { m_BoneMatrices[i].SetIdentity(); } } m_PositionValid = true; } CModel::~CModel() { rtl_FreeAligned(m_BoneMatrices); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // CalcBound: calculate the world space bounds of this model void CModel::CalcBounds() { // Need to calculate the object bounds first, if that hasn't already been done if (! (m_Anim && m_Anim->m_AnimDef)) { if (m_ObjectBounds.IsEmpty()) CalcStaticObjectBounds(); } else { if (m_Anim->m_ObjectBounds.IsEmpty()) CalcAnimatedObjectBounds(m_Anim->m_AnimDef, m_Anim->m_ObjectBounds); ENSURE(! m_Anim->m_ObjectBounds.IsEmpty()); // (if this happens, it'll be recalculating the bounds every time) m_ObjectBounds = m_Anim->m_ObjectBounds; } // Ensure the transform is set correctly before we use it ValidatePosition(); // Now transform the object-space bounds to world-space bounds m_ObjectBounds.Transform(GetTransform(), m_WorldBounds); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // CalcObjectBounds: calculate object space bounds of this model, based solely on vertex positions void CModel::CalcStaticObjectBounds() { PROFILE2("CalcStaticObjectBounds"); - m_pModelDef->GetMaxBounds(nullptr, !(m_Flags & MODELFLAG_NOLOOPANIMATION), m_ObjectBounds); + m_pModelDef->GetMaxBounds(nullptr, !(m_Flags & ModelFlag::NO_LOOP_ANIMATION), m_ObjectBounds); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // CalcAnimatedObjectBound: calculate bounds encompassing all vertex positions for given animation void CModel::CalcAnimatedObjectBounds(CSkeletonAnimDef* anim, CBoundingBoxAligned& result) { PROFILE2("CalcAnimatedObjectBounds"); - m_pModelDef->GetMaxBounds(anim, !(m_Flags & MODELFLAG_NOLOOPANIMATION), result); + m_pModelDef->GetMaxBounds(anim, !(m_Flags & ModelFlag::NO_LOOP_ANIMATION), result); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// const CBoundingBoxAligned CModel::GetWorldBoundsRec() { CBoundingBoxAligned bounds = GetWorldBounds(); for (size_t i = 0; i < m_Props.size(); ++i) bounds += m_Props[i].m_Model->GetWorldBoundsRec(); return bounds; } const CBoundingBoxAligned CModel::GetObjectSelectionBoundsRec() { CBoundingBoxAligned objBounds = GetObjectBounds(); // updates the (children-not-included) object-space bounds if necessary // now extend these bounds to include the props' selection bounds (if any) for (size_t i = 0; i < m_Props.size(); ++i) { const Prop& prop = m_Props[i]; if (prop.m_Hidden || !prop.m_Selectable) continue; // prop is hidden from rendering, so it also shouldn't be used for selection CBoundingBoxAligned propSelectionBounds = prop.m_Model->GetObjectSelectionBoundsRec(); if (propSelectionBounds.IsEmpty()) continue; // submodel does not wish to participate in selection box, exclude it // We have the prop's bounds in its own object-space; now we need to transform them so they can be properly added // to the bounds in our object-space. For that, we need the transform of the prop attachment point. // // We have the prop point information; however, it's not trivial to compute its exact location in our object-space // since it may or may not be attached to a bone (see SPropPoint), which in turn may or may not be in the middle of // an animation. The bone matrices might be of interest, but they're really only meant to be used for the animation // system and are quite opaque to use from the outside (see @ref ValidatePosition). // // However, a nice side effect of ValidatePosition is that it also computes the absolute world-space transform of // our props and sets it on their respective models. In particular, @ref ValidatePosition will compute the prop's // world-space transform as either // // T' = T x B x O // or // T' = T x O // // where T' is the prop's world-space transform, T is our world-space transform, O is the prop's local // offset/rotation matrix, and B is an optional transformation matrix of the bone the prop is attached to // (taking into account animation and everything). // // From this, it is clear that either O or B x O is the object-space transformation matrix of the prop. So, // all we need to do is apply our own inverse world-transform T^(-1) to T' to get our desired result. Luckily, // this is precomputed upon setting the transform matrix (see @ref SetTransform), so it is free to fetch. CMatrix3D propObjectTransform = prop.m_Model->GetTransform(); // T' propObjectTransform.Concatenate(GetInvTransform()); // T^(-1) x T' // Transform the prop's bounds into our object coordinate space CBoundingBoxAligned transformedPropSelectionBounds; propSelectionBounds.Transform(propObjectTransform, transformedPropSelectionBounds); objBounds += transformedPropSelectionBounds; } return objBounds; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // Update: update this model to the given time, in msec void CModel::UpdateTo(float time) { // update animation time, but don't calculate bone matrices - do that (lazily) when // something requests them; that saves some calculation work for offscreen models, // and also assures the world space, inverted bone matrices (required for normal // skinning) are up to date with respect to m_Transform m_AnimTime = time; // mark vertices as dirty SetDirty(RENDERDATA_UPDATE_VERTICES); // mark matrices as dirty InvalidatePosition(); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // InvalidatePosition void CModel::InvalidatePosition() { m_PositionValid = false; for (size_t i = 0; i < m_Props.size(); ++i) m_Props[i].m_Model->InvalidatePosition(); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // ValidatePosition: ensure that current transform and bone matrices are both uptodate void CModel::ValidatePosition() { if (m_PositionValid) { ENSURE(!m_Parent || m_Parent->m_PositionValid); return; } if (m_Parent && !m_Parent->m_PositionValid) { // Make sure we don't base our calculations on // a parent animation state that is out of date. m_Parent->ValidatePosition(); // Parent will recursively call our validation. ENSURE(m_PositionValid); return; } if (m_Anim && m_BoneMatrices) { // PROFILE( "generating bone matrices" ); ENSURE(m_pModelDef->GetNumBones() == m_Anim->m_AnimDef->GetNumKeys()); - m_Anim->m_AnimDef->BuildBoneMatrices(m_AnimTime, m_BoneMatrices, !(m_Flags & MODELFLAG_NOLOOPANIMATION)); + m_Anim->m_AnimDef->BuildBoneMatrices(m_AnimTime, m_BoneMatrices, !(m_Flags & ModelFlag::NO_LOOP_ANIMATION)); } else if (m_BoneMatrices) { // Bones but no animation - probably a buggy actor forgot to set up the animation, // so just render it in its bind pose for (size_t i = 0; i < m_pModelDef->GetNumBones(); i++) { m_BoneMatrices[i].SetIdentity(); m_BoneMatrices[i].Rotate(m_pModelDef->GetBones()[i].m_Rotation); m_BoneMatrices[i].Translate(m_pModelDef->GetBones()[i].m_Translation); } } // For CPU skinning, we precompute as much as possible so that the only // per-vertex work is a single matrix*vec multiplication. // For GPU skinning, we try to minimise CPU work by doing most computation // in the vertex shader instead. // Using g_RenderingOptions to detect CPU vs GPU is a bit hacky, // and this doesn't allow the setting to change at runtime, but there isn't // an obvious cleaner way to determine what data needs to be computed, // and GPU skinning is a rarely-used experimental feature anyway. bool worldSpaceBoneMatrices = !g_RenderingOptions.GetGPUSkinning(); bool computeBlendMatrices = !g_RenderingOptions.GetGPUSkinning(); if (m_BoneMatrices && worldSpaceBoneMatrices) { // add world-space transformation to m_BoneMatrices const CMatrix3D transform = GetTransform(); for (size_t i = 0; i < m_pModelDef->GetNumBones(); i++) m_BoneMatrices[i].Concatenate(transform); } // our own position is now valid; now we can safely update our props' positions without fearing // that doing so will cause a revalidation of this model (see recursion above). m_PositionValid = true; CMatrix3D translate; CVector3D objTranslation = m_Transform.GetTranslation(); float objectHeight = 0.0f; CmpPtr cmpTerrain(m_Simulation, SYSTEM_ENTITY); if (cmpTerrain) objectHeight = cmpTerrain->GetExactGroundLevel(objTranslation.X, objTranslation.Z); // Object height is incorrect for floating objects. We use water height instead. - if (m_Flags & MODELFLAG_FLOATONWATER) + if (m_Flags & ModelFlag::FLOAT_ON_WATER) { CmpPtr cmpWaterManager(m_Simulation, SYSTEM_ENTITY); if (cmpWaterManager) { const float waterHeight = cmpWaterManager->GetExactWaterLevel(objTranslation.X, objTranslation.Z); if (waterHeight >= objectHeight) objectHeight = waterHeight; } } // re-position and validate all props for (const Prop& prop : m_Props) { CMatrix3D proptransform = prop.m_Point->m_Transform; if (prop.m_Point->m_BoneIndex != 0xff) { CMatrix3D boneMatrix = m_BoneMatrices[prop.m_Point->m_BoneIndex]; if (!worldSpaceBoneMatrices) boneMatrix.Concatenate(GetTransform()); proptransform.Concatenate(boneMatrix); } else { // not relative to any bone; just apply world-space transformation (i.e. relative to object-space origin) proptransform.Concatenate(m_Transform); } // Adjust prop height to terrain level when needed if (cmpTerrain && (prop.m_MaxHeight != 0.f || prop.m_MinHeight != 0.f)) { const CVector3D& propTranslation = proptransform.GetTranslation(); const float propTerrain = cmpTerrain->GetExactGroundLevel(propTranslation.X, propTranslation.Z); const float translateHeight = std::min(prop.m_MaxHeight, std::max(prop.m_MinHeight, propTerrain - objectHeight)); translate.SetTranslation(0.f, translateHeight, 0.f); proptransform.Concatenate(translate); } prop.m_Model->SetTransform(proptransform); prop.m_Model->ValidatePosition(); } if (m_BoneMatrices) { for (size_t i = 0; i < m_pModelDef->GetNumBones(); i++) { m_BoneMatrices[i] = m_BoneMatrices[i] * m_pModelDef->GetInverseBindBoneMatrices()[i]; } // Note: there is a special case of joint influence, in which the vertex // is influenced by the bind-shape transform instead of a particular bone, // which we indicate with the blending bone ID set to the total number // of bones. But since we're skinning in world space, we use the model's // world space transform and store that matrix in this special index. // (see http://trac.wildfiregames.com/ticket/1012) m_BoneMatrices[m_pModelDef->GetNumBones()] = m_Transform; if (computeBlendMatrices) m_pModelDef->BlendBoneMatrices(m_BoneMatrices); } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // SetAnimation: set the given animation as the current animation on this model; // return false on error, else true bool CModel::SetAnimation(CSkeletonAnim* anim, bool once) { m_Anim = nullptr; // in case something fails if (anim) { - m_Flags &= ~MODELFLAG_NOLOOPANIMATION; + m_Flags &= ~ModelFlag::NO_LOOP_ANIMATION; if (once) - m_Flags |= MODELFLAG_NOLOOPANIMATION; + m_Flags |= ModelFlag::NO_LOOP_ANIMATION; // Not rigged or animation is not valid. if (!m_BoneMatrices || !anim->m_AnimDef) return false; if (anim->m_AnimDef->GetNumKeys() != m_pModelDef->GetNumBones()) { LOGERROR("Mismatch between model's skeleton and animation's skeleton (%s.dae has %lu model bones while the animation %s has %lu animation keys.)", m_pModelDef->GetName().string8().c_str() , static_cast(m_pModelDef->GetNumBones()), anim->m_Name.c_str(), static_cast(anim->m_AnimDef->GetNumKeys())); return false; } // Reset the cached bounds when the animation is changed. m_ObjectBounds.SetEmpty(); InvalidateBounds(); // Start anim from beginning. m_AnimTime = 0; } m_Anim = anim; return true; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // CopyAnimation void CModel::CopyAnimationFrom(CModel* source) { m_Anim = source->m_Anim; m_AnimTime = source->m_AnimTime; m_ObjectBounds.SetEmpty(); InvalidateBounds(); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // AddProp: add a prop to the model on the given point void CModel::AddProp(const SPropPoint* point, std::unique_ptr model, CObjectEntry* objectentry, float minHeight, float maxHeight, bool selectable) { // position model according to prop point position // this next call will invalidate the bounds of "model", which will in turn also invalidate the selection box model->SetTransform(point->m_Transform); model->m_Parent = this; Prop prop; prop.m_Point = point; prop.m_Model = std::move(model); prop.m_ObjectEntry = objectentry; prop.m_MinHeight = minHeight; prop.m_MaxHeight = maxHeight; prop.m_Selectable = selectable; m_Props.push_back(std::move(prop)); } void CModel::AddAmmoProp(const SPropPoint* point, std::unique_ptr model, CObjectEntry* objectentry) { AddProp(point, std::move(model), objectentry); m_AmmoPropPoint = point; m_AmmoLoadedProp = m_Props.size() - 1; m_Props[m_AmmoLoadedProp].m_Hidden = true; // we only need to invalidate the selection box here if it is based on props and their visibilities if (!m_CustomSelectionShape) m_SelectionBoxValid = false; } void CModel::ShowAmmoProp() { if (m_AmmoPropPoint == NULL) return; // Show the ammo prop, hide all others on the same prop point for (size_t i = 0; i < m_Props.size(); ++i) if (m_Props[i].m_Point == m_AmmoPropPoint) m_Props[i].m_Hidden = (i != m_AmmoLoadedProp); // we only need to invalidate the selection box here if it is based on props and their visibilities if (!m_CustomSelectionShape) m_SelectionBoxValid = false; } void CModel::HideAmmoProp() { if (m_AmmoPropPoint == NULL) return; // Hide the ammo prop, show all others on the same prop point for (size_t i = 0; i < m_Props.size(); ++i) if (m_Props[i].m_Point == m_AmmoPropPoint) m_Props[i].m_Hidden = (i == m_AmmoLoadedProp); // we only need to invalidate here if the selection box is based on props and their visibilities if (!m_CustomSelectionShape) m_SelectionBoxValid = false; } CModelAbstract* CModel::FindFirstAmmoProp() { if (m_AmmoPropPoint) return m_Props[m_AmmoLoadedProp].m_Model.get(); for (size_t i = 0; i < m_Props.size(); ++i) { CModel* propModel = m_Props[i].m_Model->ToCModel(); if (propModel) { CModelAbstract* model = propModel->FindFirstAmmoProp(); if (model) return model; } } return NULL; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // Clone: return a clone of this model std::unique_ptr CModel::Clone() const { std::unique_ptr clone = std::make_unique(m_Simulation, m_Material, m_pModelDef); clone->m_ObjectBounds = m_ObjectBounds; clone->SetAnimation(m_Anim); clone->SetFlags(m_Flags); for (size_t i = 0; i < m_Props.size(); i++) { // eek! TODO, RC - need to investigate shallow clone here if (m_AmmoPropPoint && i == m_AmmoLoadedProp) clone->AddAmmoProp(m_Props[i].m_Point, m_Props[i].m_Model->Clone(), m_Props[i].m_ObjectEntry); else clone->AddProp(m_Props[i].m_Point, m_Props[i].m_Model->Clone(), m_Props[i].m_ObjectEntry, m_Props[i].m_MinHeight, m_Props[i].m_MaxHeight, m_Props[i].m_Selectable); } return clone; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // SetTransform: set the transform on this object, and reorientate props accordingly void CModel::SetTransform(const CMatrix3D& transform) { // call base class to set transform on this object CRenderableObject::SetTransform(transform); InvalidatePosition(); } ////////////////////////////////////////////////////////////////////////// void CModel::AddFlagsRec(int flags) { m_Flags |= flags; - if (flags & MODELFLAG_IGNORE_LOS) + if (flags & ModelFlag::IGNORE_LOS) m_Material.AddShaderDefine(str_IGNORE_LOS, str_1); for (size_t i = 0; i < m_Props.size(); ++i) if (m_Props[i].m_Model->ToCModel()) m_Props[i].m_Model->ToCModel()->AddFlagsRec(flags); } void CModel::RemoveShadowsRec() { - m_Flags &= ~MODELFLAG_CASTSHADOWS; + m_Flags &= ~ModelFlag::CAST_SHADOWS; m_Material.AddShaderDefine(str_DISABLE_RECEIVE_SHADOWS, str_1); for (size_t i = 0; i < m_Props.size(); ++i) { if (m_Props[i].m_Model->ToCModel()) m_Props[i].m_Model->ToCModel()->RemoveShadowsRec(); else if (m_Props[i].m_Model->ToCModelDecal()) m_Props[i].m_Model->ToCModelDecal()->RemoveShadows(); } } void CModel::SetPlayerID(player_id_t id) { CModelAbstract::SetPlayerID(id); for (std::vector::iterator it = m_Props.begin(); it != m_Props.end(); ++it) it->m_Model->SetPlayerID(id); } void CModel::SetShadingColor(const CColor& color) { CModelAbstract::SetShadingColor(color); for (std::vector::iterator it = m_Props.begin(); it != m_Props.end(); ++it) it->m_Model->SetShadingColor(color); } Index: ps/trunk/source/graphics/Model.h =================================================================== --- ps/trunk/source/graphics/Model.h (revision 27879) +++ ps/trunk/source/graphics/Model.h (revision 27880) @@ -1,268 +1,261 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* * Mesh object with texture and skinning information */ #ifndef INCLUDED_MODEL #define INCLUDED_MODEL #include "graphics/Material.h" #include "graphics/MeshManager.h" #include "graphics/ModelAbstract.h" #include struct SPropPoint; class CObjectEntry; class CSkeletonAnim; class CSkeletonAnimDef; class CSimulation2; -#define MODELFLAG_CASTSHADOWS (1<<0) -#define MODELFLAG_NOLOOPANIMATION (1<<1) -#define MODELFLAG_SILHOUETTE_DISPLAY (1<<2) -#define MODELFLAG_SILHOUETTE_OCCLUDER (1<<3) -#define MODELFLAG_IGNORE_LOS (1<<4) -#define MODELFLAG_FLOATONWATER (1<<5) - // Holds world information for a particular instance of a model in the game. class CModel : public CModelAbstract { NONCOPYABLE(CModel); public: struct Prop { float m_MinHeight{0.0f}; float m_MaxHeight{0.0f}; /** * Location of the prop point within its parent model, relative to either a bone in the parent model or to the * parent model's origin. See the documentation for @ref SPropPoint for more details. * @see SPropPoint */ const SPropPoint* m_Point{nullptr}; /** * Pointer to the model associated with this prop. Note that the transform matrix held by this model is the full object-to-world * space transform, taking into account all parent model positioning (see @ref CModel::ValidatePosition for positioning logic). * @see CModel::ValidatePosition */ std::unique_ptr m_Model; CObjectEntry* m_ObjectEntry{nullptr}; bool m_Hidden{false}; ///< Should this prop be temporarily removed from rendering? bool m_Selectable{true}; /// < should this prop count in the selection size? }; public: CModel(const CSimulation2& simulation, const CMaterial& material, const CModelDefPtr& modeldef); ~CModel() override; /// Dynamic cast CModel* ToCModel() override { return this; } // update this model's state; 'time' is the absolute time since the start of the animation, in MS void UpdateTo(float time); // get the model's geometry data const CModelDefPtr& GetModelDef() { return m_pModelDef; } // set the model's player ID, recursively through props void SetPlayerID(player_id_t id) override; // set the models mod color void SetShadingColor(const CColor& color) override; // get the model's material const CMaterial& GetMaterial() { return m_Material; } // set the given animation as the current animation on this model bool SetAnimation(CSkeletonAnim* anim, bool once = false); // get the currently playing animation, if any CSkeletonAnim* GetAnimation() const { return m_Anim; } // set the animation state to be the same as from another; both models should // be compatible types (same type of skeleton) void CopyAnimationFrom(CModel* source); // set object flags void SetFlags(int flags) { m_Flags=flags; } // get object flags int GetFlags() const { return m_Flags; } // add object flags, recursively through props void AddFlagsRec(int flags); // remove shadow casting and receiving, recursively through props // TODO: replace with more generic shader define + flags setting void RemoveShadowsRec(); void SetTerrainDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1) override { for (size_t i = 0; i < m_Props.size(); ++i) m_Props[i].m_Model->SetTerrainDirty(i0, j0, i1, j1); } void SetEntityVariable(const std::string& name, float value) override { for (size_t i = 0; i < m_Props.size(); ++i) m_Props[i].m_Model->SetEntityVariable(name, value); } // --- WORLD/OBJECT SPACE BOUNDS ----------------------------------------------------------------- /// Overridden to calculate both the world-space and object-space bounds of this model, and stores the result in /// m_Bounds and m_ObjectBounds, respectively. void CalcBounds() override; /// Returns the object-space bounds for this model, excluding its children. const CBoundingBoxAligned& GetObjectBounds() { RecalculateBoundsIfNecessary(); // recalculates both object-space and world-space bounds if necessary return m_ObjectBounds; } const CBoundingBoxAligned GetWorldBoundsRec() override; // reimplemented here /// Auxiliary method; calculates object space bounds of this model, based solely on vertex positions, and stores /// the result in m_ObjectBounds. Called by CalcBounds (instead of CalcAnimatedObjectBounds) if it has been determined /// that the object-space bounds are static. void CalcStaticObjectBounds(); /// Auxiliary method; calculate object-space bounds encompassing all vertex positions for given animation, and stores /// the result in m_ObjectBounds. Called by CalcBounds (instead of CalcStaticBounds) if it has been determined that the /// object-space bounds need to take animations into account. void CalcAnimatedObjectBounds(CSkeletonAnimDef* anim,CBoundingBoxAligned& result); // --- SELECTION BOX/BOUNDS ---------------------------------------------------------------------- /// Reimplemented here since proper models should participate in selection boxes. const CBoundingBoxAligned GetObjectSelectionBoundsRec() override; /** * Set transform of this object. * * @note In order to ensure that all child props are updated properly, * you must call ValidatePosition(). */ void SetTransform(const CMatrix3D& transform) override; /** * Return whether this is a skinned/skeletal model. If it is, Get*BoneMatrices() * will return valid non-NULL arrays. */ bool IsSkinned() { return (m_BoneMatrices != NULL); } // return the models bone matrices; 16-byte aligned for SSE reads const CMatrix3D* GetAnimatedBoneMatrices() { ENSURE(m_PositionValid); return m_BoneMatrices; } /** * Add a prop to the model on the given point. */ void AddProp(const SPropPoint* point, std::unique_ptr model, CObjectEntry* objectentry, float minHeight = 0.f, float maxHeight = 0.f, bool selectable = true); /** * Add a prop to the model on the given point, and treat it as the ammo prop. * The prop will be hidden by default. */ void AddAmmoProp(const SPropPoint* point, std::unique_ptr model, CObjectEntry* objectentry); /** * Show the ammo prop (if any), and hide any other props on that prop point. */ void ShowAmmoProp(); /** * Hide the ammo prop (if any), and show any other props on that prop point. */ void HideAmmoProp(); /** * Find the first prop used for ammo, by this model or its own props. */ CModelAbstract* FindFirstAmmoProp(); // return prop list std::vector& GetProps() { return m_Props; } const std::vector& GetProps() const { return m_Props; } // return a clone of this model std::unique_ptr Clone() const override; /** * Ensure that both the transformation and the bone * matrices are correct for this model and all its props. */ void ValidatePosition() override; /** * Mark this model's position and bone matrices, * and all props' positions as invalid. */ void InvalidatePosition() override; private: // Needed for terrain aligned props const CSimulation2& m_Simulation; // object flags int m_Flags{0}; // model's material CMaterial m_Material; // pointer to the model's raw 3d data const CModelDefPtr m_pModelDef; // object space bounds of model - accounts for bounds of all possible animations // that can play on a model. Not always up-to-date - currently CalcBounds() // updates it when necessary. CBoundingBoxAligned m_ObjectBounds; // animation currently playing on this model, if any CSkeletonAnim* m_Anim = nullptr; // time (in MS) into the current animation float m_AnimTime{0.0f}; /** * Current state of all bones on this model; null if associated modeldef isn't skeletal. * Props may attach to these bones by means of the SPropPoint::m_BoneIndex field; in this case their * transformation matrix held is relative to the bone transformation (see @ref SPropPoint and * @ref CModel::ValidatePosition). * * @see SPropPoint */ CMatrix3D* m_BoneMatrices{nullptr}; // list of current props on model std::vector m_Props; /** * The prop point to which the ammo prop is attached, or NULL if none */ const SPropPoint* m_AmmoPropPoint{nullptr}; /** * If m_AmmoPropPoint is not NULL, then the index in m_Props of the ammo prop */ size_t m_AmmoLoadedProp{0}; }; #endif // INCLUDED_MODEL Index: ps/trunk/source/graphics/ModelAbstract.h =================================================================== --- ps/trunk/source/graphics/ModelAbstract.h (revision 27879) +++ ps/trunk/source/graphics/ModelAbstract.h (revision 27880) @@ -1,195 +1,205 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_MODELABSTRACT #define INCLUDED_MODELABSTRACT #include "graphics/Color.h" #include "graphics/RenderableObject.h" #include "maths/BoundingBoxOriented.h" #include "simulation2/helpers/Player.h" #include class CModelDummy; class CModel; class CModelDecal; class CModelParticleEmitter; +namespace ModelFlag +{ +static constexpr uint32_t CAST_SHADOWS{1 << 0}; +static constexpr uint32_t NO_LOOP_ANIMATION{1 << 1}; +static constexpr uint32_t SILHOUETTE_DISPLAY{1 << 2}; +static constexpr uint32_t SILHOUETTE_OCCLUDER{1 << 3}; +static constexpr uint32_t IGNORE_LOS{1 << 4}; +static constexpr uint32_t FLOAT_ON_WATER{1 << 5}; +} // namespace ModelFlag + /** * Abstract base class for graphical objects that are used by units, * or as props attached to other CModelAbstract objects. * This includes meshes, terrain decals, and sprites. * These objects exist in a tree hierarchy. */ class CModelAbstract : public CRenderableObject { NONCOPYABLE(CModelAbstract); public: /** * Describes a custom selection shape to be used for a model's selection box instead of the default * recursive bounding boxes. */ struct CustomSelectionShape { enum EType { /// The selection shape is determined by an oriented box of custom, user-specified size. BOX, /// The selection shape is determined by a cylinder of custom, user-specified size. CYLINDER }; EType m_Type; ///< Type of shape. float m_Size0; ///< Box width if @ref BOX, or radius if @ref CYLINDER float m_Size1; ///< Box depth if @ref BOX, or radius if @ref CYLINDER float m_Height; ///< Box height if @ref BOX, cylinder height if @ref CYLINDER }; public: CModelAbstract() : m_Parent(NULL), m_PositionValid(false), m_ShadingColor(1, 1, 1, 1), m_PlayerID(INVALID_PLAYER), m_SelectionBoxValid(false), m_CustomSelectionShape(NULL) { } virtual ~CModelAbstract() { delete m_CustomSelectionShape; // allocated and set externally by CCmpVisualActor, but our responsibility to clean up } virtual std::unique_ptr Clone() const = 0; /// Dynamic cast virtual CModelDummy* ToCModelDummy() { return nullptr; } /// Dynamic cast virtual CModel* ToCModel() { return nullptr; } /// Dynamic cast virtual CModelDecal* ToCModelDecal() { return nullptr; } /// Dynamic cast virtual CModelParticleEmitter* ToCModelParticleEmitter() { return nullptr; } // (This dynamic casting is a bit ugly, but we won't have many subclasses // and this seems the easiest way to integrate with other code that wants // type-specific processing) /// Returns world space bounds of this object and all child objects. virtual const CBoundingBoxAligned GetWorldBoundsRec() { return GetWorldBounds(); } // default implementation /** * Returns the world-space selection box of this model. Used primarily for hittesting against against a selection ray. The * returned selection box may be empty to indicate that it does not wish to participate in the selection process. */ virtual const CBoundingBoxOriented& GetSelectionBox(); virtual void InvalidateBounds() { m_BoundsValid = false; // a call to this method usually means that the model's transform has changed, i.e. it has moved or rotated, so we'll also // want to update the selection box accordingly regardless of the shape it is built from. m_SelectionBoxValid = false; } /// Sets a custom selection shape as described by a @p descriptor. Argument may be NULL /// if you wish to keep the default behaviour of using the recursively-calculated bounding boxes. void SetCustomSelectionShape(CustomSelectionShape* descriptor) { if (m_CustomSelectionShape != descriptor) { m_CustomSelectionShape = descriptor; m_SelectionBoxValid = false; // update the selection box when it is next requested } } /** * Returns the (object-space) bounds that should be used to construct a selection box for this model and its children. * May return an empty bound to indicate that this model and its children should not be selectable themselves, or should * not be included in its parent model's selection box. This method is used for constructing the default selection boxes, * as opposed to any boxes of custom shape specified by @ref m_CustomSelectionShape. * * If you wish your model type to be included in selection boxes, override this method and have it return the object-space * bounds of itself, augmented recursively (via this method) with the object-space selection bounds of its children. */ virtual const CBoundingBoxAligned GetObjectSelectionBoundsRec() { return CBoundingBoxAligned::EMPTY; } /** * Called when terrain has changed in the given inclusive bounds. * Might call SetDirty if the change affects this model. */ virtual void SetTerrainDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1) = 0; /** * Called when the entity tries to set some variable to affect the display of this model * and/or its child objects. */ virtual void SetEntityVariable(const std::string& UNUSED(name), float UNUSED(value)) { } /** * Ensure that both the transformation and the bone matrices are correct for this model and all its props. */ virtual void ValidatePosition() = 0; /** * Mark this model's position and bone matrices, and all props' positions as invalid. */ virtual void InvalidatePosition() = 0; virtual void SetPlayerID(player_id_t id) { m_PlayerID = id; } // get the model's player ID; initial default is INVALID_PLAYER virtual player_id_t GetPlayerID() const { return m_PlayerID; } virtual void SetShadingColor(const CColor& color) { m_ShadingColor = color; } virtual const CColor& GetShadingColor() const { return m_ShadingColor; } protected: void CalcSelectionBox(); public: /// If non-null, points to the model that we are attached to. CModelAbstract* m_Parent; /// True if both transform and and bone matrices are valid. bool m_PositionValid; player_id_t m_PlayerID; /// Modulating color CColor m_ShadingColor; protected: /// Selection box for this model. CBoundingBoxOriented m_SelectionBox; /// Is the current selection box valid? bool m_SelectionBoxValid; /// Pointer to a descriptor for a custom-defined selection box shape. If no custom selection box is required, this is NULL /// and the standard recursive-bounding-box-based selection box is used. Otherwise, a custom selection box described by this /// field will be used. /// @see SetCustomSelectionShape CustomSelectionShape* m_CustomSelectionShape; }; #endif // INCLUDED_MODELABSTRACT Index: ps/trunk/source/graphics/ObjectEntry.cpp =================================================================== --- ps/trunk/source/graphics/ObjectEntry.cpp (revision 27879) +++ ps/trunk/source/graphics/ObjectEntry.cpp (revision 27880) @@ -1,318 +1,318 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ObjectEntry.h" #include "graphics/Decal.h" #include "graphics/Material.h" #include "graphics/MaterialManager.h" #include "graphics/MeshManager.h" #include "graphics/Model.h" #include "graphics/ModelDef.h" #include "graphics/ModelDummy.h" #include "graphics/ObjectBase.h" #include "graphics/ObjectManager.h" #include "graphics/ParticleManager.h" #include "graphics/SkeletonAnim.h" #include "graphics/SkeletonAnimManager.h" #include "graphics/TextureManager.h" #include "lib/rand.h" #include "ps/CLogger.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/SceneRenderer.h" #include "simulation2/Simulation2.h" #include CObjectEntry::CObjectEntry(const std::shared_ptr& base, const CSimulation2& simulation) : m_Base(base), m_Color(1.0f, 1.0f, 1.0f, 1.0f), m_Simulation(simulation) { } CObjectEntry::~CObjectEntry() = default; bool CObjectEntry::BuildVariation(const std::vector*>& completeSelections, const std::vector& variationKey, CObjectManager& objectManager) { CObjectBase::Variation variation = m_Base->BuildVariation(variationKey); // Copy the chosen data onto this model: for (std::multimap::iterator it = variation.samplers.begin(); it != variation.samplers.end(); ++it) m_Samplers.push_back(it->second); m_ModelName = variation.model; if (! variation.color.empty()) { std::stringstream str; str << variation.color; int r, g, b; if (! (str >> r >> g >> b)) // Any trailing data is ignored LOGERROR("Actor '%s' has invalid RGB color '%s'", m_Base->GetIdentifier(), variation.color); else m_Color = CColor(r/255.0f, g/255.0f, b/255.0f, 1.0f); } if (variation.decal.m_SizeX && variation.decal.m_SizeZ) { CMaterial material = g_Renderer.GetSceneRenderer().GetMaterialManager().LoadMaterial(m_Base->m_Material); for (const CObjectBase::Samp& samp : m_Samplers) { CTextureProperties textureProps(samp.m_SamplerFile); // TODO: replace all samplers by CLAMP_TO_EDGE after decals // refactoring. Also we need to avoid custom border colors. textureProps.SetAddressMode( samp.m_SamplerName == str_baseTex ? Renderer::Backend::Sampler::AddressMode::CLAMP_TO_BORDER : Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); // TODO: Should check which renderpath is selected and only preload the necessary textures. texture->Prefetch(); material.AddSampler(CMaterial::TextureSampler(samp.m_SamplerName, texture)); } SDecal decal(material, variation.decal.m_SizeX, variation.decal.m_SizeZ, variation.decal.m_Angle, variation.decal.m_OffsetX, variation.decal.m_OffsetZ, m_Base->m_Properties.m_FloatOnWater); m_Model = std::make_unique(objectManager.GetTerrain(), decal); return true; } if (!variation.particles.empty()) { m_Model = std::make_unique(g_Renderer.GetSceneRenderer().GetParticleManager().LoadEmitterType(variation.particles)); return true; } if (variation.model.empty()) { m_Model = std::make_unique(); return true; } std::vector props; for (std::multimap::iterator it = variation.props.begin(); it != variation.props.end(); ++it) props.push_back(it->second); // Build the model: // try and create a model CModelDefPtr modeldef (objectManager.GetMeshManager().GetMesh(m_ModelName)); if (!modeldef) { LOGERROR("CObjectEntry::BuildVariation(): Model %s failed to load", m_ModelName.string8()); return false; } // delete old model, create new CMaterial material = g_Renderer.GetSceneRenderer().GetMaterialManager().LoadMaterial(m_Base->m_Material); material.AddStaticUniform("objectColor", CVector4D(m_Color.r, m_Color.g, m_Color.b, m_Color.a)); if (m_Samplers.empty()) LOGERROR("Actor '%s' has no textures.", m_Base->GetIdentifier()); for (const CObjectBase::Samp& samp : m_Samplers) { CTextureProperties textureProps(samp.m_SamplerFile); textureProps.SetAddressMode(Renderer::Backend::Sampler::AddressMode::CLAMP_TO_EDGE); CTexturePtr texture = g_Renderer.GetTextureManager().CreateTexture(textureProps); // if we've loaded this model we're probably going to render it soon, so prefetch its texture. // All textures are prefetched even in the fixed pipeline, including the normal maps etc. // TODO: Should check which renderpath is selected and only preload the necessary textures. texture->Prefetch(); material.AddSampler(CMaterial::TextureSampler(samp.m_SamplerName, texture)); } std::unique_ptr newModel = std::make_unique(m_Simulation, material, modeldef); CModel* model = newModel.get(); m_Model = std::move(newModel); for (const CStrIntern& requSampName : model->GetMaterial().GetRequiredSampler()) { if (std::find_if(m_Samplers.begin(), m_Samplers.end(), [&](const CObjectBase::Samp& sampler) { return sampler.m_SamplerName == requSampName; }) == m_Samplers.end()) LOGERROR("Actor %s: required texture sampler %s not found (material %s)", m_Base->GetIdentifier(), requSampName.string().c_str(), m_Base->m_Material.string8().c_str()); } // calculate initial object space bounds, based on vertex positions model->CalcStaticObjectBounds(); // load the animations for (std::multimap::iterator it = variation.anims.begin(); it != variation.anims.end(); ++it) { CStr name = it->first.LowerCase(); if (it->second.m_FileName.empty()) continue; std::unique_ptr anim = objectManager.GetSkeletonAnimManager().BuildAnimation( it->second.m_FileName, name, it->second.m_ID, it->second.m_Frequency, it->second.m_Speed, it->second.m_ActionPos, it->second.m_ActionPos2, it->second.m_SoundPos); if (anim) m_Animations.emplace(name, std::move(anim)); } // ensure there's always an idle animation if (m_Animations.find("idle") == m_Animations.end()) { std::unique_ptr anim = std::make_unique(); anim->m_Name = "idle"; anim->m_ID = ""; anim->m_AnimDef = NULL; anim->m_Frequency = 0; anim->m_Speed = 0.f; anim->m_ActionPos = 0.f; anim->m_ActionPos2 = 0.f; anim->m_SoundPos = 0.f; SkeletonAnimMap::const_iterator it = m_Animations.emplace("idle", std::move(anim)); // Ignore errors, since they're probably saying this is a non-animated model model->SetAnimation(it->second.get()); } else { // start up idling if (!model->SetAnimation(GetRandomAnimation("idle"))) LOGERROR("Failed to set idle animation in model \"%s\"", m_ModelName.string8()); } // build props - TODO, RC - need to fix up bounds here // TODO: Make sure random variations get handled correctly when a prop fails for (const CObjectBase::Prop& prop : props) { // Pluck out the special attachpoint 'projectile' if (prop.m_PropPointName == "projectile") { m_ProjectileModelName = prop.m_ModelName; continue; } CObjectEntry* oe = nullptr; if (auto [success, actorDef] = objectManager.FindActorDef(prop.m_ModelName.c_str()); success) oe = objectManager.FindObjectVariation(actorDef.GetBase(m_Base->m_QualityLevel), completeSelections); if (!oe) { LOGERROR("Failed to build prop model \"%s\" on actor \"%s\"", utf8_from_wstring(prop.m_ModelName), m_Base->GetIdentifier()); continue; } // If we don't have a projectile but this prop does (e.g. it's our rider), then // use that as our projectile too if (m_ProjectileModelName.empty() && !oe->m_ProjectileModelName.empty()) m_ProjectileModelName = oe->m_ProjectileModelName; CStr ppn = prop.m_PropPointName; bool isAmmo = false; // Handle the special attachpoint 'loaded-' if (ppn.Find("loaded-") == 0) { ppn = prop.m_PropPointName.substr(7); isAmmo = true; } const SPropPoint* proppoint = modeldef->FindPropPoint(ppn.c_str()); if (proppoint) { std::unique_ptr propmodel = oe->m_Model->Clone(); if (propmodel->ToCModel()) propmodel->ToCModel()->SetAnimation(oe->GetRandomAnimation("idle")); if (isAmmo) model->AddAmmoProp(proppoint, std::move(propmodel), oe); else model->AddProp(proppoint, std::move(propmodel), oe, prop.m_minHeight, prop.m_maxHeight, prop.m_selectable); } else LOGERROR("Failed to find matching prop point called \"%s\" in model \"%s\" for actor \"%s\"", ppn, m_ModelName.string8(), m_Base->GetIdentifier()); } // Setup flags. if (m_Base->m_Properties.m_CastShadows) { - model->SetFlags(model->GetFlags() | MODELFLAG_CASTSHADOWS); + model->SetFlags(model->GetFlags() | ModelFlag::CAST_SHADOWS); } if (m_Base->m_Properties.m_FloatOnWater) { - model->SetFlags(model->GetFlags() | MODELFLAG_FLOATONWATER); + model->SetFlags(model->GetFlags() | ModelFlag::FLOAT_ON_WATER); } return true; } CSkeletonAnim* CObjectEntry::GetRandomAnimation(const CStr& animationName, const CStr& ID) const { std::vector anims = GetAnimations(animationName, ID); int totalFreq = 0; for (CSkeletonAnim* anim : anims) totalFreq += anim->m_Frequency; if (totalFreq == 0) return anims[rand(0, anims.size())]; int r = rand(0, totalFreq); for (CSkeletonAnim* anim : anims) { r -= anim->m_Frequency; if (r < 0) return anim; } return NULL; } std::vector CObjectEntry::GetAnimations(const CStr& animationName, const CStr& ID) const { std::vector anims; SkeletonAnimMap::const_iterator lower = m_Animations.lower_bound(animationName); SkeletonAnimMap::const_iterator upper = m_Animations.upper_bound(animationName); for (SkeletonAnimMap::const_iterator it = lower; it != upper; ++it) { if (ID.empty() || it->second->m_ID == ID) anims.push_back(it->second.get()); } if (anims.empty()) { lower = m_Animations.lower_bound("idle"); upper = m_Animations.upper_bound("idle"); for (SkeletonAnimMap::const_iterator it = lower; it != upper; ++it) anims.push_back(it->second.get()); } ENSURE(!anims.empty()); return anims; } Index: ps/trunk/source/graphics/Unit.cpp =================================================================== --- ps/trunk/source/graphics/Unit.cpp (revision 27879) +++ ps/trunk/source/graphics/Unit.cpp (revision 27880) @@ -1,159 +1,159 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "Unit.h" #include "graphics/Model.h" #include "graphics/ObjectBase.h" #include "graphics/ObjectEntry.h" #include "graphics/ObjectManager.h" #include "graphics/SkeletonAnim.h" #include "graphics/SkeletonAnimDef.h" #include "graphics/UnitAnimation.h" #include "ps/CLogger.h" CUnit::CUnit(CObjectManager& objectManager, const CActorDef& actor, const entity_id_t id, const uint32_t seed) : m_ObjectManager(objectManager), m_Actor(actor), m_ID(id), m_Seed(seed), m_Animation(nullptr) { /** * When entity selections change, we might end up with a different layout in terms of variants/groups, * which means the random key calculation might end up with different results for the same seed. * This is bad, as it means entities randomly change appearence when changing e.g. animation. * To fix this, we'll initially pick a random and complete specification based on our seed, * and then pass that as the lowest priority selections. Thus, if the actor files are properly specified, * we can ensure that the entities will look the same no matter what happens. */ SetActorSelections(m_Actor.PickSelectionsAtRandom(m_Seed)); // Calls ReloadObject(). } CUnit::~CUnit() { delete m_Animation; } CUnit* CUnit::Create(const CStrW& actorName, const entity_id_t id, const uint32_t seed, CObjectManager& objectManager) { auto [success, actor] = objectManager.FindActorDef(actorName); UNUSED2(success); CUnit* unit = new CUnit(objectManager, actor, id, seed); if (!unit->m_Model) { delete unit; return nullptr; } return unit; } void CUnit::UpdateModel(float frameTime) { if (m_Animation) m_Animation->Update(frameTime*1000.0f); } void CUnit::SetEntitySelection(const CStr& key, const CStr& selection) { CStr selection_lc = selection.LowerCase(); if (m_EntitySelections[key] == selection_lc) return; m_EntitySelections[key] = selection_lc; ReloadObject(); } void CUnit::SetEntitySelection(const std::map& selections) { for (const std::pair& s : selections) m_EntitySelections[s.first] = s.second.LowerCase(); ReloadObject(); } void CUnit::SetActorSelections(const std::set& selections) { m_ActorSelections = selections; ReloadObject(); } void CUnit::ReloadObject() { std::set entitySelections; for (const std::pair& selection : m_EntitySelections) entitySelections.insert(selection.second); std::vector> selections; selections.push_back(entitySelections); selections.push_back(m_ActorSelections); // randomly select any remain selections necessary to completely identify a variation (e.g., the new selection // made might define some additional props that require a random variant choice). Also, FindObjectVariation // expects the selectors passed to it to be complete. // see http://trac.wildfiregames.com/ticket/979 // If these selections give a different object, change this unit to use it // Use the entity ID as randomization seed (same as when the unit was first created) CObjectEntry* newObject = m_ObjectManager.FindObjectVariation(&m_Actor, selections, m_Seed); if (!newObject) { LOGERROR("Error loading object variation (actor: %s)", m_Actor.GetPathname().string8()); // Don't delete the unit, don't override our current (valid) state. return; } if (!m_Object) { m_Object = newObject; m_Model = newObject->m_Model->Clone(); if (m_Model->ToCModel()) m_Animation = new CUnitAnimation(m_ID, m_Model->ToCModel(), m_Object); } else if (m_Object && newObject != m_Object) { // Clone the new object's base (non-instance) model std::unique_ptr newModel = newObject->m_Model->Clone(); // Copy the old instance-specific settings from the old model to the new instance newModel->SetTransform(m_Model->GetTransform()); newModel->SetPlayerID(m_Model->GetPlayerID()); if (newModel->ToCModel() && m_Model->ToCModel()) { newModel->ToCModel()->CopyAnimationFrom(m_Model->ToCModel()); // Copy flags that belong to this model instance (not those defined by the actor XML) - int instanceFlags = (MODELFLAG_SILHOUETTE_DISPLAY|MODELFLAG_SILHOUETTE_OCCLUDER|MODELFLAG_IGNORE_LOS) & m_Model->ToCModel()->GetFlags(); + int instanceFlags = (ModelFlag::SILHOUETTE_DISPLAY | ModelFlag::SILHOUETTE_OCCLUDER | ModelFlag::IGNORE_LOS) & m_Model->ToCModel()->GetFlags(); newModel->ToCModel()->AddFlagsRec(instanceFlags); } m_Model = std::move(newModel); m_Object = newObject; if (m_Model->ToCModel()) { if (m_Animation) m_Animation->ReloadUnit(m_Model->ToCModel(), m_Object); // TODO: maybe this should try to preserve animation state? else m_Animation = new CUnitAnimation(m_ID, m_Model->ToCModel(), m_Object); } else { SAFE_DELETE(m_Animation); } } } Index: ps/trunk/source/graphics/tests/test_Model.h =================================================================== --- ps/trunk/source/graphics/tests/test_Model.h (revision 27879) +++ ps/trunk/source/graphics/tests/test_Model.h (revision 27880) @@ -1,83 +1,83 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "graphics/Material.h" #include "graphics/Model.h" #include "graphics/ModelDef.h" #include "graphics/ShaderDefines.h" #include "ps/CStrInternStatic.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include class TestModel : public CxxTest::TestSuite { public: bool HasShaderDefine(const CShaderDefines& defines, CStrIntern define) { const auto& map = defines.GetMap(); const auto it = map.find(define); return it != map.end() && it->second == str_1; } bool HasMaterialDefine(CModel* model, CStrIntern define) { return HasShaderDefine(model->GetMaterial().GetShaderDefines(), define); } void test_model_with_flags() { CMaterial material{}; CSimulation2 simulation{nullptr, g_ScriptContext, nullptr}; // TODO: load a proper mock for modeldef. CModelDefPtr modeldef = std::make_shared(); std::unique_ptr model = std::make_unique(simulation, material, modeldef); SPropPoint propPoint{}; model->AddProp(&propPoint, std::make_unique(simulation, material, modeldef), nullptr); - model->AddFlagsRec(MODELFLAG_IGNORE_LOS); + model->AddFlagsRec(ModelFlag::IGNORE_LOS); model->RemoveShadowsRec(); TS_ASSERT(HasMaterialDefine(model.get(), str_DISABLE_RECEIVE_SHADOWS)); TS_ASSERT(HasMaterialDefine(model.get(), str_IGNORE_LOS)); for (const CModel::Prop& prop : model->GetProps()) { TS_ASSERT(prop.m_Model->ToCModel()); TS_ASSERT(HasMaterialDefine(prop.m_Model->ToCModel(), str_DISABLE_RECEIVE_SHADOWS)); TS_ASSERT(HasMaterialDefine(prop.m_Model->ToCModel(), str_IGNORE_LOS)); } std::unique_ptr clonedModel = model->Clone(); TS_ASSERT(clonedModel->ToCModel()); TS_ASSERT(HasMaterialDefine(clonedModel->ToCModel(), str_DISABLE_RECEIVE_SHADOWS)); TS_ASSERT(HasMaterialDefine(clonedModel->ToCModel(), str_IGNORE_LOS)); TS_ASSERT_EQUALS(model->GetProps().size(), clonedModel->ToCModel()->GetProps().size()); for (const CModel::Prop& prop : clonedModel->ToCModel()->GetProps()) { TS_ASSERT(prop.m_Model->ToCModel()); TS_ASSERT(HasMaterialDefine(prop.m_Model->ToCModel(), str_DISABLE_RECEIVE_SHADOWS)); TS_ASSERT(HasMaterialDefine(prop.m_Model->ToCModel(), str_IGNORE_LOS)); } } }; Index: ps/trunk/source/renderer/SceneRenderer.cpp =================================================================== --- ps/trunk/source/renderer/SceneRenderer.cpp (revision 27879) +++ ps/trunk/source/renderer/SceneRenderer.cpp (revision 27880) @@ -1,1207 +1,1207 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "SceneRenderer.h" #include "graphics/Camera.h" #include "graphics/Decal.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/LOSTexture.h" #include "graphics/MaterialManager.h" #include "graphics/MiniMapTexture.h" #include "graphics/Model.h" #include "graphics/ModelDef.h" #include "graphics/ParticleManager.h" #include "graphics/Patch.h" #include "graphics/ShaderManager.h" #include "graphics/TerritoryTexture.h" #include "graphics/Terrain.h" #include "graphics/Texture.h" #include "graphics/TextureManager.h" #include "maths/Matrix3D.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/Game.h" #include "ps/Profile.h" #include "ps/World.h" #include "renderer/backend/IDevice.h" #include "renderer/DebugRenderer.h" #include "renderer/HWLightingModelRenderer.h" #include "renderer/InstancingModelRenderer.h" #include "renderer/ModelRenderer.h" #include "renderer/OverlayRenderer.h" #include "renderer/ParticleRenderer.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/RenderModifiers.h" #include "renderer/ShadowMap.h" #include "renderer/SilhouetteRenderer.h" #include "renderer/SkyManager.h" #include "renderer/TerrainOverlay.h" #include "renderer/TerrainRenderer.h" #include "renderer/WaterManager.h" #include struct SScreenRect { int x1, y1, x2, y2; }; /** * Struct CSceneRendererInternals: Truly hide data that is supposed to be hidden * in this structure so it won't even appear in header files. */ class CSceneRenderer::Internals { NONCOPYABLE(Internals); public: Internals(Renderer::Backend::IDevice* device) : waterManager(device), shadow(device) { } ~Internals() = default; /// Water manager WaterManager waterManager; /// Sky manager SkyManager skyManager; /// Terrain renderer TerrainRenderer terrainRenderer; /// Overlay renderer OverlayRenderer overlayRenderer; /// Particle manager CParticleManager particleManager; /// Particle renderer ParticleRenderer particleRenderer; /// Material manager CMaterialManager materialManager; /// Shadow map ShadowMap shadow; SilhouetteRenderer silhouetteRenderer; /// Various model renderers struct Models { // NOTE: The current renderer design (with ModelRenderer, ModelVertexRenderer, // RenderModifier, etc) is mostly a relic of an older design that implemented // the different materials and rendering modes through extensive subclassing // and hooking objects together in various combinations. // The new design uses the CShaderManager API to abstract away the details // of rendering, and uses a data-driven approach to materials, so there are // now a small number of generic subclasses instead of many specialised subclasses, // but most of the old infrastructure hasn't been refactored out yet and leads to // some unwanted complexity. // Submitted models are split on two axes: // - Normal vs Transp[arent] - alpha-blended models are stored in a separate // list so we can draw them above/below the alpha-blended water plane correctly // - Skinned vs Unskinned - with hardware lighting we don't need to // duplicate mesh data per model instance (except for skinned models), // so non-skinned models get different ModelVertexRenderers ModelRendererPtr NormalSkinned; ModelRendererPtr NormalUnskinned; // == NormalSkinned if unskinned shader instancing not supported ModelRendererPtr TranspSkinned; ModelRendererPtr TranspUnskinned; // == TranspSkinned if unskinned shader instancing not supported ModelVertexRendererPtr VertexRendererShader; ModelVertexRendererPtr VertexInstancingShader; ModelVertexRendererPtr VertexGPUSkinningShader; LitRenderModifierPtr ModShader; } Model; CShaderDefines globalContext; /** * Renders all non-alpha-blended models with the given context. */ void CallModelRenderers( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup, int flags) { CShaderDefines contextSkinned = context; if (g_RenderingOptions.GetGPUSkinning()) { contextSkinned.Add(str_USE_INSTANCING, str_1); contextSkinned.Add(str_USE_GPU_SKINNING, str_1); } Model.NormalSkinned->Render(deviceCommandContext, Model.ModShader, contextSkinned, cullGroup, flags); if (Model.NormalUnskinned != Model.NormalSkinned) { CShaderDefines contextUnskinned = context; contextUnskinned.Add(str_USE_INSTANCING, str_1); Model.NormalUnskinned->Render(deviceCommandContext, Model.ModShader, contextUnskinned, cullGroup, flags); } } /** * Renders all alpha-blended models with the given context. */ void CallTranspModelRenderers( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup, int flags) { CShaderDefines contextSkinned = context; if (g_RenderingOptions.GetGPUSkinning()) { contextSkinned.Add(str_USE_INSTANCING, str_1); contextSkinned.Add(str_USE_GPU_SKINNING, str_1); } Model.TranspSkinned->Render(deviceCommandContext, Model.ModShader, contextSkinned, cullGroup, flags); if (Model.TranspUnskinned != Model.TranspSkinned) { CShaderDefines contextUnskinned = context; contextUnskinned.Add(str_USE_INSTANCING, str_1); Model.TranspUnskinned->Render(deviceCommandContext, Model.ModShader, contextUnskinned, cullGroup, flags); } } }; CSceneRenderer::CSceneRenderer(Renderer::Backend::IDevice* device) { m = std::make_unique(device); m_TerrainRenderMode = SOLID; m_WaterRenderMode = SOLID; m_ModelRenderMode = SOLID; m_OverlayRenderMode = SOLID; m_DisplayTerrainPriorities = false; m_LightEnv = nullptr; m_CurrentScene = nullptr; } CSceneRenderer::~CSceneRenderer() { // We no longer UnloadWaterTextures here - // that is the responsibility of the module that asked for // them to be loaded (i.e. CGameView). m.reset(); } void CSceneRenderer::ReloadShaders(Renderer::Backend::IDevice* device) { m->globalContext = CShaderDefines(); if (g_RenderingOptions.GetShadows()) { m->globalContext.Add(str_USE_SHADOW, str_1); if (device->GetBackend() == Renderer::Backend::Backend::GL_ARB && device->GetCapabilities().ARBShadersShadow) { m->globalContext.Add(str_USE_FP_SHADOW, str_1); } if (g_RenderingOptions.GetShadowPCF()) m->globalContext.Add(str_USE_SHADOW_PCF, str_1); const int cascadeCount = m->shadow.GetCascadeCount(); ENSURE(1 <= cascadeCount && cascadeCount <= 4); const CStrIntern cascadeCountStr[5] = {str_0, str_1, str_2, str_3, str_4}; m->globalContext.Add(str_SHADOWS_CASCADE_COUNT, cascadeCountStr[cascadeCount]); #if !CONFIG2_GLES m->globalContext.Add(str_USE_SHADOW_SAMPLER, str_1); #endif } m->globalContext.Add(str_RENDER_DEBUG_MODE, RenderDebugModeEnum::ToString(g_RenderingOptions.GetRenderDebugMode())); if (device->GetBackend() != Renderer::Backend::Backend::GL_ARB && g_RenderingOptions.GetFog()) m->globalContext.Add(str_USE_FOG, str_1); m->Model.ModShader = LitRenderModifierPtr(new ShaderRenderModifier()); ENSURE(g_RenderingOptions.GetRenderPath() != RenderPath::FIXED); m->Model.VertexRendererShader = ModelVertexRendererPtr(new ShaderModelVertexRenderer()); m->Model.VertexInstancingShader = ModelVertexRendererPtr(new InstancingModelRenderer(false, device->GetBackend() != Renderer::Backend::Backend::GL_ARB)); if (g_RenderingOptions.GetGPUSkinning()) // TODO: should check caps and GLSL etc too { m->Model.VertexGPUSkinningShader = ModelVertexRendererPtr(new InstancingModelRenderer(true, device->GetBackend() != Renderer::Backend::Backend::GL_ARB)); m->Model.NormalSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexGPUSkinningShader)); m->Model.TranspSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexGPUSkinningShader)); } else { m->Model.VertexGPUSkinningShader.reset(); m->Model.NormalSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexRendererShader)); m->Model.TranspSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexRendererShader)); } m->Model.NormalUnskinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexInstancingShader)); m->Model.TranspUnskinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexInstancingShader)); } void CSceneRenderer::Initialize() { // Let component renderers perform one-time initialization after graphics capabilities and // the shader path have been determined. m->waterManager.Initialize(); m->terrainRenderer.Initialize(); m->overlayRenderer.Initialize(); } // resize renderer view void CSceneRenderer::Resize(int UNUSED(width), int UNUSED(height)) { // need to recreate the shadow map object to resize the shadow texture m->shadow.RecreateTexture(); m->waterManager.RecreateOrLoadTexturesIfNeeded(); } void CSceneRenderer::BeginFrame() { // choose model renderers for this frame m->Model.ModShader->SetShadowMap(&m->shadow); m->Model.ModShader->SetLightEnv(m_LightEnv); } void CSceneRenderer::SetSimulation(CSimulation2* simulation) { // set current simulation context for terrain renderer m->terrainRenderer.SetSimulation(simulation); } void CSceneRenderer::RenderShadowMap( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context) { PROFILE3_GPU("shadow map"); GPU_SCOPED_LABEL(deviceCommandContext, "Render shadow map"); CShaderDefines shadowsContext = context; shadowsContext.Add(str_PASS_SHADOWS, str_1); CShaderDefines contextCast = shadowsContext; contextCast.Add(str_MODE_SHADOWCAST, str_1); m->shadow.BeginRender(deviceCommandContext); const int cascadeCount = m->shadow.GetCascadeCount(); ENSURE(0 <= cascadeCount && cascadeCount <= 4); for (int cascade = 0; cascade < cascadeCount; ++cascade) { m->shadow.PrepareCamera(deviceCommandContext, cascade); const int cullGroup = CULL_SHADOWS_CASCADE_0 + cascade; { PROFILE("render patches"); m->terrainRenderer.RenderPatches(deviceCommandContext, cullGroup, shadowsContext); } { PROFILE("render models"); - m->CallModelRenderers(deviceCommandContext, contextCast, cullGroup, MODELFLAG_CASTSHADOWS); + m->CallModelRenderers(deviceCommandContext, contextCast, cullGroup, ModelFlag::CAST_SHADOWS); } { PROFILE("render transparent models"); - m->CallTranspModelRenderers(deviceCommandContext, contextCast, cullGroup, MODELFLAG_CASTSHADOWS); + m->CallTranspModelRenderers(deviceCommandContext, contextCast, cullGroup, ModelFlag::CAST_SHADOWS); } } m->shadow.EndRender(deviceCommandContext); } void CSceneRenderer::RenderPatches( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup) { PROFILE3_GPU("patches"); GPU_SCOPED_LABEL(deviceCommandContext, "Render patches"); // Switch on wireframe if we need it. CShaderDefines localContext = context; if (m_TerrainRenderMode == WIREFRAME) localContext.Add(str_MODE_WIREFRAME, str_1); // Render all the patches, including blend pass. m->terrainRenderer.RenderTerrainShader(deviceCommandContext, localContext, cullGroup, g_RenderingOptions.GetShadows() ? &m->shadow : nullptr); if (m_TerrainRenderMode == EDGED_FACES) { localContext.Add(str_MODE_WIREFRAME, str_1); // Edged faces: need to make a second pass over the data. // Render tiles edges. m->terrainRenderer.RenderPatches( deviceCommandContext, cullGroup, localContext, CColor(0.5f, 0.5f, 1.0f, 1.0f)); // Render outline of each patch. m->terrainRenderer.RenderOutlines(deviceCommandContext, cullGroup); } } void CSceneRenderer::RenderModels( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup) { PROFILE3_GPU("models"); GPU_SCOPED_LABEL(deviceCommandContext, "Render models"); int flags = 0; CShaderDefines localContext = context; if (m_ModelRenderMode == WIREFRAME) localContext.Add(str_MODE_WIREFRAME, str_1); m->CallModelRenderers(deviceCommandContext, localContext, cullGroup, flags); if (m_ModelRenderMode == EDGED_FACES) { localContext.Add(str_MODE_WIREFRAME_SOLID, str_1); m->CallModelRenderers(deviceCommandContext, localContext, cullGroup, flags); } } void CSceneRenderer::RenderTransparentModels( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, int cullGroup, ETransparentMode transparentMode) { PROFILE3_GPU("transparent models"); GPU_SCOPED_LABEL(deviceCommandContext, "Render transparent models"); int flags = 0; CShaderDefines contextOpaque = context; contextOpaque.Add(str_ALPHABLEND_PASS_OPAQUE, str_1); CShaderDefines contextBlend = context; contextBlend.Add(str_ALPHABLEND_PASS_BLEND, str_1); if (m_ModelRenderMode == WIREFRAME) { contextOpaque.Add(str_MODE_WIREFRAME, str_1); contextBlend.Add(str_MODE_WIREFRAME, str_1); } if (transparentMode == TRANSPARENT || transparentMode == TRANSPARENT_OPAQUE) m->CallTranspModelRenderers(deviceCommandContext, contextOpaque, cullGroup, flags); if (transparentMode == TRANSPARENT || transparentMode == TRANSPARENT_BLEND) m->CallTranspModelRenderers(deviceCommandContext, contextBlend, cullGroup, flags); if (m_ModelRenderMode == EDGED_FACES) { CShaderDefines contextWireframe = contextOpaque; contextWireframe.Add(str_MODE_WIREFRAME, str_1); m->CallTranspModelRenderers(deviceCommandContext, contextWireframe, cullGroup, flags); } } // SetObliqueFrustumClipping: change the near plane to the given clip plane (in world space) // Based on code from Game Programming Gems 5, from http://www.terathon.com/code/oblique.html // - worldPlane is a clip plane in world space (worldPlane.Dot(v) >= 0 for any vector v passing the clipping test) void CSceneRenderer::SetObliqueFrustumClipping(CCamera& camera, const CVector4D& worldPlane) const { // First, we'll convert the given clip plane to camera space, then we'll // Get the view matrix and normal matrix (top 3x3 part of view matrix) CMatrix3D normalMatrix = camera.GetOrientation().GetTranspose(); CVector4D camPlane = normalMatrix.Transform(worldPlane); CMatrix3D matrix = camera.GetProjection(); // Calculate the clip-space corner point opposite the clipping plane // as (sgn(camPlane.x), sgn(camPlane.y), 1, 1) and // transform it into camera space by multiplying it // by the inverse of the projection matrix CVector4D q; q.X = (Sign(camPlane.X) - matrix[8] / matrix[11]) / matrix[0]; q.Y = (Sign(camPlane.Y) - matrix[9] / matrix[11]) / matrix[5]; q.Z = 1.0f / matrix[11]; q.W = (1.0f - matrix[10] / matrix[11]) / matrix[14]; // Calculate the scaled plane vector CVector4D c = camPlane * (2.0f * matrix[11] / camPlane.Dot(q)); // Replace the third row of the projection matrix matrix[2] = c.X; matrix[6] = c.Y; matrix[10] = c.Z - matrix[11]; matrix[14] = c.W; // Load it back into the camera camera.SetProjection(matrix); } void CSceneRenderer::ComputeReflectionCamera(CCamera& camera, const CBoundingBoxAligned& scissor) const { WaterManager& wm = m->waterManager; CMatrix3D projection; if (m_ViewCamera.GetProjectionType() == CCamera::ProjectionType::PERSPECTIVE) { const float aspectRatio = 1.0f; // Expand fov slightly since ripples can reflect parts of the scene that // are slightly outside the normal camera view, and we want to avoid any // noticeable edge-filtering artifacts projection.SetPerspective(m_ViewCamera.GetFOV() * 1.05f, aspectRatio, m_ViewCamera.GetNearPlane(), m_ViewCamera.GetFarPlane()); } else projection = m_ViewCamera.GetProjection(); camera = m_ViewCamera; // Temporarily change the camera to one that is reflected. // Also, for texturing purposes, make it render to a view port the size of the // water texture, stretch the image according to our aspect ratio so it covers // the whole screen despite being rendered into a square, and cover slightly more // of the view so we can see wavy reflections of slightly off-screen objects. camera.m_Orientation.Scale(1, -1, 1); camera.m_Orientation.Translate(0, 2 * wm.m_WaterHeight, 0); camera.UpdateFrustum(scissor); // Clip slightly above the water to improve reflections of objects on the water // when the reflections are distorted. camera.ClipFrustum(CVector4D(0, 1, 0, -wm.m_WaterHeight + 2.0f)); SViewPort vp; vp.m_Height = wm.m_RefTextureSize; vp.m_Width = wm.m_RefTextureSize; vp.m_X = 0; vp.m_Y = 0; camera.SetViewPort(vp); camera.SetProjection(projection); CMatrix3D scaleMat; scaleMat.SetScaling(g_Renderer.GetHeight() / static_cast(std::max(1, g_Renderer.GetWidth())), 1.0f, 1.0f); camera.SetProjection(scaleMat * camera.GetProjection()); CVector4D camPlane(0, 1, 0, -wm.m_WaterHeight + 0.5f); SetObliqueFrustumClipping(camera, camPlane); } void CSceneRenderer::ComputeRefractionCamera(CCamera& camera, const CBoundingBoxAligned& scissor) const { WaterManager& wm = m->waterManager; CMatrix3D projection; if (m_ViewCamera.GetProjectionType() == CCamera::ProjectionType::PERSPECTIVE) { const float aspectRatio = 1.0f; // Expand fov slightly since ripples can reflect parts of the scene that // are slightly outside the normal camera view, and we want to avoid any // noticeable edge-filtering artifacts projection.SetPerspective(m_ViewCamera.GetFOV() * 1.05f, aspectRatio, m_ViewCamera.GetNearPlane(), m_ViewCamera.GetFarPlane()); } else projection = m_ViewCamera.GetProjection(); camera = m_ViewCamera; // Temporarily change the camera to make it render to a view port the size of the // water texture, stretch the image according to our aspect ratio so it covers // the whole screen despite being rendered into a square, and cover slightly more // of the view so we can see wavy refractions of slightly off-screen objects. camera.UpdateFrustum(scissor); camera.ClipFrustum(CVector4D(0, -1, 0, wm.m_WaterHeight + 0.5f)); // add some to avoid artifacts near steep shores. SViewPort vp; vp.m_Height = wm.m_RefTextureSize; vp.m_Width = wm.m_RefTextureSize; vp.m_X = 0; vp.m_Y = 0; camera.SetViewPort(vp); camera.SetProjection(projection); CMatrix3D scaleMat; scaleMat.SetScaling(g_Renderer.GetHeight() / static_cast(std::max(1, g_Renderer.GetWidth())), 1.0f, 1.0f); camera.SetProjection(scaleMat * camera.GetProjection()); } // RenderReflections: render the water reflections to the reflection texture void CSceneRenderer::RenderReflections( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, const CBoundingBoxAligned& scissor) { PROFILE3_GPU("water reflections"); GPU_SCOPED_LABEL(deviceCommandContext, "Render water reflections"); WaterManager& wm = m->waterManager; // Remember old camera CCamera normalCamera = m_ViewCamera; ComputeReflectionCamera(m_ViewCamera, scissor); const CBoundingBoxAligned reflectionScissor = m->terrainRenderer.ScissorWater(CULL_DEFAULT, m_ViewCamera); if (reflectionScissor.IsEmpty()) { m_ViewCamera = normalCamera; return; } // Save the model-view-projection matrix so the shaders can use it for projective texturing wm.m_ReflectionMatrix = m_ViewCamera.GetViewProjection(); if (deviceCommandContext->GetDevice()->GetBackend() == Renderer::Backend::Backend::VULKAN) { CMatrix3D flip; flip.SetIdentity(); flip._22 = -1.0f; wm.m_ReflectionMatrix = flip * wm.m_ReflectionMatrix; } float vpHeight = wm.m_RefTextureSize; float vpWidth = wm.m_RefTextureSize; SScreenRect screenScissor; screenScissor.x1 = static_cast(floor((reflectionScissor[0].X * 0.5f + 0.5f) * vpWidth)); screenScissor.y1 = static_cast(floor((reflectionScissor[0].Y * 0.5f + 0.5f) * vpHeight)); screenScissor.x2 = static_cast(ceil((reflectionScissor[1].X * 0.5f + 0.5f) * vpWidth)); screenScissor.y2 = static_cast(ceil((reflectionScissor[1].Y * 0.5f + 0.5f) * vpHeight)); deviceCommandContext->BeginFramebufferPass(wm.m_ReflectionFramebuffer.get()); Renderer::Backend::IDeviceCommandContext::Rect viewportRect{}; viewportRect.width = vpWidth; viewportRect.height = vpHeight; deviceCommandContext->SetViewports(1, &viewportRect); Renderer::Backend::IDeviceCommandContext::Rect scissorRect; scissorRect.x = screenScissor.x1; scissorRect.y = screenScissor.y1; scissorRect.width = screenScissor.x2 - screenScissor.x1; scissorRect.height = screenScissor.y2 - screenScissor.y1; deviceCommandContext->SetScissors(1, &scissorRect); CShaderDefines reflectionsContext = context; reflectionsContext.Add(str_PASS_REFLECTIONS, str_1); // Render terrain and models RenderPatches(deviceCommandContext, reflectionsContext, CULL_REFLECTIONS); RenderModels(deviceCommandContext, reflectionsContext, CULL_REFLECTIONS); RenderTransparentModels(deviceCommandContext, reflectionsContext, CULL_REFLECTIONS, TRANSPARENT); // Particles are always oriented to face the camera in the vertex shader, // so they don't need the inverted cull face. if (g_RenderingOptions.GetParticles()) { RenderParticles(deviceCommandContext, CULL_REFLECTIONS); } deviceCommandContext->SetScissors(0, nullptr); deviceCommandContext->EndFramebufferPass(); // Reset old camera m_ViewCamera = normalCamera; } // RenderRefractions: render the water refractions to the refraction texture void CSceneRenderer::RenderRefractions( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context, const CBoundingBoxAligned &scissor) { PROFILE3_GPU("water refractions"); GPU_SCOPED_LABEL(deviceCommandContext, "Render water refractions"); WaterManager& wm = m->waterManager; // Remember old camera CCamera normalCamera = m_ViewCamera; ComputeRefractionCamera(m_ViewCamera, scissor); const CBoundingBoxAligned refractionScissor = m->terrainRenderer.ScissorWater(CULL_DEFAULT, m_ViewCamera); if (refractionScissor.IsEmpty()) { m_ViewCamera = normalCamera; return; } CVector4D camPlane(0, -1, 0, wm.m_WaterHeight + 2.0f); SetObliqueFrustumClipping(m_ViewCamera, camPlane); // Save the model-view-projection matrix so the shaders can use it for projective texturing wm.m_RefractionMatrix = m_ViewCamera.GetViewProjection(); wm.m_RefractionProjInvMatrix = m_ViewCamera.GetProjection().GetInverse(); wm.m_RefractionViewInvMatrix = m_ViewCamera.GetOrientation(); if (deviceCommandContext->GetDevice()->GetBackend() == Renderer::Backend::Backend::VULKAN) { CMatrix3D flip; flip.SetIdentity(); flip._22 = -1.0f; wm.m_RefractionMatrix = flip * wm.m_RefractionMatrix; wm.m_RefractionProjInvMatrix = wm.m_RefractionProjInvMatrix * flip; } float vpHeight = wm.m_RefTextureSize; float vpWidth = wm.m_RefTextureSize; SScreenRect screenScissor; screenScissor.x1 = static_cast(floor((refractionScissor[0].X * 0.5f + 0.5f) * vpWidth)); screenScissor.y1 = static_cast(floor((refractionScissor[0].Y * 0.5f + 0.5f) * vpHeight)); screenScissor.x2 = static_cast(ceil((refractionScissor[1].X * 0.5f + 0.5f) * vpWidth)); screenScissor.y2 = static_cast(ceil((refractionScissor[1].Y * 0.5f + 0.5f) * vpHeight)); deviceCommandContext->BeginFramebufferPass(wm.m_RefractionFramebuffer.get()); Renderer::Backend::IDeviceCommandContext::Rect viewportRect{}; viewportRect.width = vpWidth; viewportRect.height = vpHeight; deviceCommandContext->SetViewports(1, &viewportRect); Renderer::Backend::IDeviceCommandContext::Rect scissorRect; scissorRect.x = screenScissor.x1; scissorRect.y = screenScissor.y1; scissorRect.width = screenScissor.x2 - screenScissor.x1; scissorRect.height = screenScissor.y2 - screenScissor.y1; deviceCommandContext->SetScissors(1, &scissorRect); // Render terrain and models RenderPatches(deviceCommandContext, context, CULL_REFRACTIONS); // Render debug-related terrain overlays to make it visible under water. ITerrainOverlay::RenderOverlaysBeforeWater(deviceCommandContext); RenderModels(deviceCommandContext, context, CULL_REFRACTIONS); RenderTransparentModels(deviceCommandContext, context, CULL_REFRACTIONS, TRANSPARENT_OPAQUE); deviceCommandContext->SetScissors(0, nullptr); deviceCommandContext->EndFramebufferPass(); // Reset old camera m_ViewCamera = normalCamera; } void CSceneRenderer::RenderSilhouettes( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CShaderDefines& context) { PROFILE3_GPU("silhouettes"); GPU_SCOPED_LABEL(deviceCommandContext, "Render silhouettes"); CShaderDefines contextOccluder = context; contextOccluder.Add(str_MODE_SILHOUETTEOCCLUDER, str_1); CShaderDefines contextDisplay = context; contextDisplay.Add(str_MODE_SILHOUETTEDISPLAY, str_1); // Render silhouettes of units hidden behind terrain or occluders. // To avoid breaking the standard rendering of alpha-blended objects, this // has to be done in a separate pass. // First we render all occluders into depth, then render all units with // inverted depth test so any behind an occluder will get drawn in a constant // color. // TODO: do we need clear here? deviceCommandContext->ClearFramebuffer(false, true, true); // Render occluders: { PROFILE("render patches"); m->terrainRenderer.RenderPatches(deviceCommandContext, CULL_SILHOUETTE_OCCLUDER, contextOccluder); } { PROFILE("render model occluders"); m->CallModelRenderers(deviceCommandContext, contextOccluder, CULL_SILHOUETTE_OCCLUDER, 0); } { PROFILE("render transparent occluders"); m->CallTranspModelRenderers(deviceCommandContext, contextOccluder, CULL_SILHOUETTE_OCCLUDER, 0); } // Since we can't sort, we'll use the stencil buffer to ensure we only draw // a pixel once (using the color of whatever model happens to be drawn first). { PROFILE("render model casters"); m->CallModelRenderers(deviceCommandContext, contextDisplay, CULL_SILHOUETTE_CASTER, 0); } { PROFILE("render transparent casters"); m->CallTranspModelRenderers(deviceCommandContext, contextDisplay, CULL_SILHOUETTE_CASTER, 0); } } void CSceneRenderer::RenderParticles( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, int cullGroup) { PROFILE3_GPU("particles"); GPU_SCOPED_LABEL(deviceCommandContext, "Render particles"); m->particleRenderer.RenderParticles( deviceCommandContext, cullGroup, m_ModelRenderMode == WIREFRAME); if (m_ModelRenderMode == EDGED_FACES) { m->particleRenderer.RenderParticles( deviceCommandContext, cullGroup, true); m->particleRenderer.RenderBounds(cullGroup); } } void CSceneRenderer::PrepareSubmissions( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CBoundingBoxAligned& waterScissor) { PROFILE3("prepare submissions"); GPU_SCOPED_LABEL(deviceCommandContext, "Prepare submissions"); m->skyManager.LoadAndUploadSkyTexturesIfNeeded(deviceCommandContext); GetScene().GetLOSTexture().InterpolateLOS(deviceCommandContext); GetScene().GetTerritoryTexture().UpdateIfNeeded(deviceCommandContext); GetScene().GetMiniMapTexture().Render( deviceCommandContext, GetScene().GetLOSTexture(), GetScene().GetTerritoryTexture()); CShaderDefines context = m->globalContext; // Prepare model renderers { PROFILE3("prepare models"); m->Model.NormalSkinned->PrepareModels(); m->Model.TranspSkinned->PrepareModels(); if (m->Model.NormalUnskinned != m->Model.NormalSkinned) m->Model.NormalUnskinned->PrepareModels(); if (m->Model.TranspUnskinned != m->Model.TranspSkinned) m->Model.TranspUnskinned->PrepareModels(); } m->terrainRenderer.PrepareForRendering(); m->overlayRenderer.PrepareForRendering(); m->particleRenderer.PrepareForRendering(context); { PROFILE3("upload models"); m->Model.NormalSkinned->UploadModels(deviceCommandContext); m->Model.TranspSkinned->UploadModels(deviceCommandContext); if (m->Model.NormalUnskinned != m->Model.NormalSkinned) m->Model.NormalUnskinned->UploadModels(deviceCommandContext); if (m->Model.TranspUnskinned != m->Model.TranspSkinned) m->Model.TranspUnskinned->UploadModels(deviceCommandContext); } m->overlayRenderer.Upload(deviceCommandContext); m->particleRenderer.Upload(deviceCommandContext); if (g_RenderingOptions.GetShadows()) { RenderShadowMap(deviceCommandContext, context); } if (m->waterManager.m_RenderWater) { if (waterScissor.GetVolume() > 0 && m->waterManager.WillRenderFancyWater()) { m->waterManager.UpdateQuality(); PROFILE3_GPU("water scissor"); if (g_RenderingOptions.GetWaterReflection()) RenderReflections(deviceCommandContext, context, waterScissor); if (g_RenderingOptions.GetWaterRefraction()) RenderRefractions(deviceCommandContext, context, waterScissor); if (g_RenderingOptions.GetWaterFancyEffects()) m->terrainRenderer.RenderWaterFoamOccluders(deviceCommandContext, CULL_DEFAULT); } } } void CSceneRenderer::RenderSubmissions( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, const CBoundingBoxAligned& waterScissor) { PROFILE3("render submissions"); GPU_SCOPED_LABEL(deviceCommandContext, "Render submissions"); CShaderDefines context = m->globalContext; constexpr int cullGroup = CULL_DEFAULT; m->skyManager.RenderSky(deviceCommandContext); // render submitted patches and models RenderPatches(deviceCommandContext, context, cullGroup); // render debug-related terrain overlays ITerrainOverlay::RenderOverlaysBeforeWater(deviceCommandContext); // render other debug-related overlays before water (so they can be seen when underwater) m->overlayRenderer.RenderOverlaysBeforeWater(deviceCommandContext); RenderModels(deviceCommandContext, context, cullGroup); // render water if (m->waterManager.m_RenderWater && g_Game && waterScissor.GetVolume() > 0) { if (m->waterManager.WillRenderFancyWater()) { // Render transparent stuff, but only the solid parts that can occlude block water. RenderTransparentModels(deviceCommandContext, context, cullGroup, TRANSPARENT_OPAQUE); m->terrainRenderer.RenderWater(deviceCommandContext, context, cullGroup, &m->shadow); // Render transparent stuff again, but only the blended parts that overlap water. RenderTransparentModels(deviceCommandContext, context, cullGroup, TRANSPARENT_BLEND); } else { m->terrainRenderer.RenderWater(deviceCommandContext, context, cullGroup, &m->shadow); // Render transparent stuff, so it can overlap models/terrain. RenderTransparentModels(deviceCommandContext, context, cullGroup, TRANSPARENT); } } else { // render transparent stuff, so it can overlap models/terrain RenderTransparentModels(deviceCommandContext, context, cullGroup, TRANSPARENT); } // render debug-related terrain overlays ITerrainOverlay::RenderOverlaysAfterWater(deviceCommandContext, cullGroup); // render some other overlays after water (so they can be displayed on top of water) m->overlayRenderer.RenderOverlaysAfterWater(deviceCommandContext); // particles are transparent so render after water if (g_RenderingOptions.GetParticles()) { RenderParticles(deviceCommandContext, cullGroup); } // render debug lines if (g_RenderingOptions.GetDisplayFrustum()) DisplayFrustum(); if (g_RenderingOptions.GetDisplayShadowsFrustum()) m->shadow.RenderDebugBounds(); m->silhouetteRenderer.RenderDebugBounds(deviceCommandContext); } void CSceneRenderer::EndFrame() { // empty lists m->terrainRenderer.EndFrame(); m->overlayRenderer.EndFrame(); m->particleRenderer.EndFrame(); m->silhouetteRenderer.EndFrame(); // Finish model renderers m->Model.NormalSkinned->EndFrame(); m->Model.TranspSkinned->EndFrame(); if (m->Model.NormalUnskinned != m->Model.NormalSkinned) m->Model.NormalUnskinned->EndFrame(); if (m->Model.TranspUnskinned != m->Model.TranspSkinned) m->Model.TranspUnskinned->EndFrame(); } void CSceneRenderer::DisplayFrustum() { g_Renderer.GetDebugRenderer().DrawCameraFrustum(m_CullCamera, CColor(1.0f, 1.0f, 1.0f, 0.25f), 2); g_Renderer.GetDebugRenderer().DrawCameraFrustum(m_CullCamera, CColor(1.0f, 1.0f, 1.0f, 1.0f), 2, true); } // Text overlay rendering void CSceneRenderer::RenderTextOverlays(CCanvas2D& canvas) { PROFILE3_GPU("text overlays"); if (m_DisplayTerrainPriorities) m->terrainRenderer.RenderPriorities(canvas, CULL_DEFAULT); } // SetSceneCamera: setup projection and transform of camera and adjust viewport to current view // The camera always represents the actual camera used to render a scene, not any virtual camera // used for shadow rendering or reflections. void CSceneRenderer::SetSceneCamera(const CCamera& viewCamera, const CCamera& cullCamera) { m_ViewCamera = viewCamera; m_CullCamera = cullCamera; if (g_RenderingOptions.GetShadows()) m->shadow.SetupFrame(m_CullCamera, m_LightEnv->GetSunDir()); } void CSceneRenderer::Submit(CPatch* patch) { if (m_CurrentCullGroup == CULL_DEFAULT) { m->shadow.AddShadowReceiverBound(patch->GetWorldBounds()); m->silhouetteRenderer.AddOccluder(patch); } if (CULL_SHADOWS_CASCADE_0 <= m_CurrentCullGroup && m_CurrentCullGroup <= CULL_SHADOWS_CASCADE_3) { const int cascade = m_CurrentCullGroup - CULL_SHADOWS_CASCADE_0; m->shadow.AddShadowCasterBound(cascade, patch->GetWorldBounds()); } m->terrainRenderer.Submit(m_CurrentCullGroup, patch); } void CSceneRenderer::Submit(SOverlayLine* overlay) { // Overlays are only needed in the default cull group for now, // so just ignore submissions to any other group if (m_CurrentCullGroup == CULL_DEFAULT) m->overlayRenderer.Submit(overlay); } void CSceneRenderer::Submit(SOverlayTexturedLine* overlay) { if (m_CurrentCullGroup == CULL_DEFAULT) m->overlayRenderer.Submit(overlay); } void CSceneRenderer::Submit(SOverlaySprite* overlay) { if (m_CurrentCullGroup == CULL_DEFAULT) m->overlayRenderer.Submit(overlay); } void CSceneRenderer::Submit(SOverlayQuad* overlay) { if (m_CurrentCullGroup == CULL_DEFAULT) m->overlayRenderer.Submit(overlay); } void CSceneRenderer::Submit(SOverlaySphere* overlay) { if (m_CurrentCullGroup == CULL_DEFAULT) m->overlayRenderer.Submit(overlay); } void CSceneRenderer::Submit(CModelDecal* decal) { // Decals can't cast shadows since they're flat on the terrain. // They can receive shadows, but the terrain under them will have // already been passed to AddShadowCasterBound, so don't bother // doing it again here. m->terrainRenderer.Submit(m_CurrentCullGroup, decal); } void CSceneRenderer::Submit(CParticleEmitter* emitter) { m->particleRenderer.Submit(m_CurrentCullGroup, emitter); } void CSceneRenderer::SubmitNonRecursive(CModel* model) { if (m_CurrentCullGroup == CULL_DEFAULT) { m->shadow.AddShadowReceiverBound(model->GetWorldBounds()); - if (model->GetFlags() & MODELFLAG_SILHOUETTE_OCCLUDER) + if (model->GetFlags() & ModelFlag::SILHOUETTE_OCCLUDER) m->silhouetteRenderer.AddOccluder(model); - if (model->GetFlags() & MODELFLAG_SILHOUETTE_DISPLAY) + if (model->GetFlags() & ModelFlag::SILHOUETTE_DISPLAY) m->silhouetteRenderer.AddCaster(model); } if (CULL_SHADOWS_CASCADE_0 <= m_CurrentCullGroup && m_CurrentCullGroup <= CULL_SHADOWS_CASCADE_3) { - if (!(model->GetFlags() & MODELFLAG_CASTSHADOWS)) + if (!(model->GetFlags() & ModelFlag::CAST_SHADOWS)) return; const int cascade = m_CurrentCullGroup - CULL_SHADOWS_CASCADE_0; m->shadow.AddShadowCasterBound(cascade, model->GetWorldBounds()); } bool requiresSkinning = (model->GetModelDef()->GetNumBones() != 0); if (model->GetMaterial().UsesAlphaBlending()) { if (requiresSkinning) m->Model.TranspSkinned->Submit(m_CurrentCullGroup, model); else m->Model.TranspUnskinned->Submit(m_CurrentCullGroup, model); } else { if (requiresSkinning) m->Model.NormalSkinned->Submit(m_CurrentCullGroup, model); else m->Model.NormalUnskinned->Submit(m_CurrentCullGroup, model); } } void CSceneRenderer::PrepareScene( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, Scene& scene) { m_CurrentScene = &scene; CFrustum frustum = m_CullCamera.GetFrustum(); m_CurrentCullGroup = CULL_DEFAULT; scene.EnumerateObjects(frustum, this); m->particleManager.RenderSubmit(*this, frustum); if (g_RenderingOptions.GetSilhouettes()) { m->silhouetteRenderer.ComputeSubmissions(m_ViewCamera); m_CurrentCullGroup = CULL_DEFAULT; m->silhouetteRenderer.RenderSubmitOverlays(*this); m_CurrentCullGroup = CULL_SILHOUETTE_OCCLUDER; m->silhouetteRenderer.RenderSubmitOccluders(*this); m_CurrentCullGroup = CULL_SILHOUETTE_CASTER; m->silhouetteRenderer.RenderSubmitCasters(*this); } if (g_RenderingOptions.GetShadows()) { for (int cascade = 0; cascade < m->shadow.GetCascadeCount(); ++cascade) { m_CurrentCullGroup = CULL_SHADOWS_CASCADE_0 + cascade; const CFrustum shadowFrustum = m->shadow.GetShadowCasterCullFrustum(cascade); scene.EnumerateObjects(shadowFrustum, this); } } if (m->waterManager.m_RenderWater) { m_WaterScissor = m->terrainRenderer.ScissorWater(CULL_DEFAULT, m_ViewCamera); if (m_WaterScissor.GetVolume() > 0 && m->waterManager.WillRenderFancyWater()) { if (g_RenderingOptions.GetWaterReflection()) { m_CurrentCullGroup = CULL_REFLECTIONS; CCamera reflectionCamera; ComputeReflectionCamera(reflectionCamera, m_WaterScissor); scene.EnumerateObjects(reflectionCamera.GetFrustum(), this); } if (g_RenderingOptions.GetWaterRefraction()) { m_CurrentCullGroup = CULL_REFRACTIONS; CCamera refractionCamera; ComputeRefractionCamera(refractionCamera, m_WaterScissor); scene.EnumerateObjects(refractionCamera.GetFrustum(), this); } // Render the waves to the Fancy effects texture m->waterManager.RenderWaves(deviceCommandContext, frustum); } } else m_WaterScissor = CBoundingBoxAligned{}; m_CurrentCullGroup = -1; PrepareSubmissions(deviceCommandContext, m_WaterScissor); } void CSceneRenderer::RenderScene( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { ENSURE(m_CurrentScene); RenderSubmissions(deviceCommandContext, m_WaterScissor); } void CSceneRenderer::RenderSceneOverlays( Renderer::Backend::IDeviceCommandContext* deviceCommandContext) { if (g_RenderingOptions.GetSilhouettes()) { RenderSilhouettes(deviceCommandContext, m->globalContext); } m->silhouetteRenderer.RenderDebugOverlays(deviceCommandContext); // Render overlays that should appear on top of all other objects. m->overlayRenderer.RenderForegroundOverlays(deviceCommandContext, m_ViewCamera); m_CurrentScene = nullptr; } Scene& CSceneRenderer::GetScene() { ENSURE(m_CurrentScene); return *m_CurrentScene; } void CSceneRenderer::MakeShadersDirty() { m->waterManager.m_NeedsReloading = true; } WaterManager& CSceneRenderer::GetWaterManager() { return m->waterManager; } SkyManager& CSceneRenderer::GetSkyManager() { return m->skyManager; } CParticleManager& CSceneRenderer::GetParticleManager() { return m->particleManager; } TerrainRenderer& CSceneRenderer::GetTerrainRenderer() { return m->terrainRenderer; } CMaterialManager& CSceneRenderer::GetMaterialManager() { return m->materialManager; } ShadowMap& CSceneRenderer::GetShadowMap() { return m->shadow; } void CSceneRenderer::ResetState() { // Clear all emitters, that were created in previous games GetParticleManager().ClearUnattachedEmitters(); } Index: ps/trunk/source/simulation2/components/CCmpVisualActor.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpVisualActor.cpp (revision 27879) +++ ps/trunk/source/simulation2/components/CCmpVisualActor.cpp (revision 27880) @@ -1,787 +1,787 @@ /* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpVisual.h" #include "simulation2/MessageTypes.h" #include "simulation2/serialization/SerializedTypes.h" #include "ICmpFootprint.h" #include "ICmpIdentity.h" #include "ICmpMirage.h" #include "ICmpOwnership.h" #include "ICmpPosition.h" #include "ICmpTemplateManager.h" #include "ICmpTerrain.h" #include "ICmpUnitMotion.h" #include "ICmpUnitRenderer.h" #include "ICmpValueModificationManager.h" #include "ICmpVisibility.h" #include "ICmpSound.h" #include "graphics/Decal.h" #include "graphics/Model.h" #include "graphics/ObjectBase.h" #include "graphics/ObjectEntry.h" #include "graphics/Unit.h" #include "graphics/UnitAnimation.h" #include "graphics/UnitManager.h" #include "maths/BoundingSphere.h" #include "maths/Frustum.h" #include "maths/Matrix3D.h" #include "maths/Vector3D.h" #include "ps/CLogger.h" #include "ps/GameSetup/Config.h" #include "renderer/Scene.h" class CCmpVisualActor final : public ICmpVisual { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_InterpolatedPositionChanged); componentManager.SubscribeToMessageType(MT_OwnershipChanged); componentManager.SubscribeToMessageType(MT_ValueModification); componentManager.SubscribeToMessageType(MT_Create); componentManager.SubscribeToMessageType(MT_Destroy); } DEFAULT_COMPONENT_ALLOCATOR(VisualActor) private: std::wstring m_BaseActorName, m_ActorName; bool m_IsFoundationActor; // Not initialized in non-visual mode CUnit* m_Unit; CModelAbstract::CustomSelectionShape* m_ShapeDescriptor = nullptr; fixed m_R, m_G, m_B; // shading color // Current animation state std::string m_AnimName; bool m_AnimOnce; fixed m_AnimSpeed; std::wstring m_SoundGroup; fixed m_AnimDesync; fixed m_AnimSyncRepeatTime; // 0.0 if not synced fixed m_AnimSyncOffsetTime; std::map m_VariantSelections; u32 m_Seed; // seed used for random variations bool m_ConstructionPreview; bool m_VisibleInAtlasOnly; bool m_IsActorOnly; // an in-world entity should not have this or it might not be rendered. bool m_SilhouetteDisplay; bool m_SilhouetteOccluder; bool m_DisableShadows; ICmpUnitRenderer::tag_t m_ModelTag; public: static std::string GetSchema() { return "Display the unit using the engine's actor system." "" "units/hellenes/infantry_spearman_b.xml" "" "" "structures/hellenes/barracks.xml" "structures/fndn_4x4.xml" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "0.0" "" "" "" "" "0.0" "" "" "" "" "0.0" "" "" "" "" "" "" "0.0" "" "" "" "" "0.0" "" "" "" "" "" "" "" "" ""; } void Init(const CParamNode& paramNode) override { m_Unit = NULL; m_R = m_G = m_B = fixed::FromInt(1); m_ConstructionPreview = paramNode.GetChild("ConstructionPreview").IsOk(); m_Seed = GetEntityId(); m_IsFoundationActor = paramNode.GetChild("Foundation").IsOk() && paramNode.GetChild("FoundationActor").IsOk(); m_BaseActorName = paramNode.GetChild(m_IsFoundationActor ? "FoundationActor" : "Actor").ToWString(); ParseActorName(m_BaseActorName); m_VisibleInAtlasOnly = paramNode.GetChild("VisibleInAtlasOnly").ToBool(); m_IsActorOnly = paramNode.GetChild("ActorOnly").IsOk(); m_SilhouetteDisplay = paramNode.GetChild("SilhouetteDisplay").ToBool(); m_SilhouetteOccluder = paramNode.GetChild("SilhouetteOccluder").ToBool(); m_DisableShadows = paramNode.GetChild("DisableShadows").ToBool(); // Initialize the model's selection shape descriptor. This currently relies on the component initialization order; the // Footprint component must be initialized before this component (VisualActor) to support the ability to use the footprint // shape for the selection box (instead of the default recursive bounding box). See TypeList.h for the order in // which components are initialized; if for whatever reason you need to get rid of this dependency, you can always just // initialize the selection shape descriptor on-demand. InitSelectionShapeDescriptor(paramNode); } void Deinit() override { if (m_Unit) { GetSimContext().GetUnitManager().DeleteUnit(m_Unit); m_Unit = NULL; } } template void SerializeCommon(S& serialize) { serialize.NumberFixed_Unbounded("r", m_R); serialize.NumberFixed_Unbounded("g", m_G); serialize.NumberFixed_Unbounded("b", m_B); serialize.StringASCII("anim name", m_AnimName, 0, 256); serialize.Bool("anim once", m_AnimOnce); serialize.NumberFixed_Unbounded("anim speed", m_AnimSpeed); serialize.String("sound group", m_SoundGroup, 0, 256); serialize.NumberFixed_Unbounded("anim desync", m_AnimDesync); serialize.NumberFixed_Unbounded("anim sync repeat time", m_AnimSyncRepeatTime); serialize.NumberFixed_Unbounded("anim sync offset time", m_AnimSyncOffsetTime); Serializer(serialize, "variation", m_VariantSelections); serialize.NumberU32_Unbounded("seed", m_Seed); serialize.String("actor", m_ActorName, 0, 256); // TODO: store actor variables? } void Serialize(ISerializer& serialize) override { // TODO: store the actor name, if !debug and it differs from the template if (serialize.IsDebug()) { serialize.String("base actor", m_BaseActorName, 0, 256); } SerializeCommon(serialize); } void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) override { Init(paramNode); u32 oldSeed = GetActorSeed(); SerializeCommon(deserialize); InitModel(); // If we serialized a different seed or different actor, reload actor if (oldSeed != GetActorSeed() || m_BaseActorName != m_ActorName) ReloadActor(); else ReloadUnitAnimation(); if (m_Unit) { CmpPtr cmpOwnership(GetEntityHandle()); if (cmpOwnership) m_Unit->GetModel().SetPlayerID(cmpOwnership->GetOwner()); } } void HandleMessage(const CMessage& msg, bool UNUSED(global)) override { switch (msg.GetType()) { case MT_OwnershipChanged: { RecomputeActorName(); if (!m_Unit) break; const CMessageOwnershipChanged& msgData = static_cast (msg); m_Unit->GetModel().SetPlayerID(msgData.to); break; } case MT_ValueModification: { // Mirages don't respond to technology modifications. CmpPtr cmpMirage(GetEntityHandle()); if (cmpMirage) return; const CMessageValueModification& msgData = static_cast (msg); if (msgData.component != L"VisualActor") break; RecomputeActorName(); break; } case MT_InterpolatedPositionChanged: { const CMessageInterpolatedPositionChanged& msgData = static_cast (msg); if (m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); cmpModelRenderer->UpdateUnitPos(m_ModelTag, msgData.inWorld, msgData.pos0, msgData.pos1); } break; } case MT_Create: { InitModel(); SelectAnimation("idle"); break; } case MT_Destroy: { if (m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); cmpModelRenderer->RemoveUnit(m_ModelTag); m_ModelTag = ICmpUnitRenderer::tag_t(); } break; } } } CBoundingBoxAligned GetBounds() const override { if (!m_Unit) return CBoundingBoxAligned::EMPTY; return m_Unit->GetModel().GetWorldBounds(); } CUnit* GetUnit() override { return m_Unit; } CBoundingBoxOriented GetSelectionBox() const override { if (!m_Unit) return CBoundingBoxOriented::EMPTY; return m_Unit->GetModel().GetSelectionBox(); } CVector3D GetPosition() const override { if (!m_Unit) return CVector3D(0, 0, 0); return m_Unit->GetModel().GetTransform().GetTranslation(); } std::wstring GetProjectileActor() const override { if (!m_Unit) return L""; return m_Unit->GetObject().m_ProjectileModelName; } CFixedVector3D GetProjectileLaunchPoint() const override { if (!m_Unit) return CFixedVector3D(); if (m_Unit->GetModel().ToCModel()) { // Ensure the prop transforms are correct CmpPtr cmpUnitRenderer(GetSystemEntity()); CmpPtr cmpPosition(GetEntityHandle()); if (cmpUnitRenderer && cmpPosition) { float frameOffset = cmpUnitRenderer->GetFrameOffset(); CMatrix3D transform(cmpPosition->GetInterpolatedTransform(frameOffset)); m_Unit->GetModel().SetTransform(transform); m_Unit->GetModel().ValidatePosition(); } CModelAbstract* ammo = m_Unit->GetModel().ToCModel()->FindFirstAmmoProp(); if (ammo) { CVector3D vector = ammo->GetTransform().GetTranslation(); return CFixedVector3D(fixed::FromFloat(vector.X), fixed::FromFloat(vector.Y), fixed::FromFloat(vector.Z)); } } return CFixedVector3D(); } void SetVariant(const CStr& key, const CStr& selection) override { if (m_VariantSelections[key] == selection) return; m_VariantSelections[key] = selection; if (m_Unit) { m_Unit->SetEntitySelection(key, selection); if (m_Unit->GetAnimation()) m_Unit->GetAnimation()->ReloadAnimation(); } } std::string GetAnimationName() const override { return m_AnimName; } void SelectAnimation(const std::string& name, bool once = false, fixed speed = fixed::FromInt(1)) override { m_AnimName = name; m_AnimOnce = once; m_AnimSpeed = speed; m_SoundGroup = L""; m_AnimDesync = fixed::FromInt(1)/20; // TODO: make this an argument m_AnimSyncRepeatTime = fixed::Zero(); m_AnimSyncOffsetTime = fixed::Zero(); SetVariant("animation", m_AnimName); CmpPtr cmpSound(GetEntityHandle()); if (cmpSound) m_SoundGroup = cmpSound->GetSoundGroup(wstring_from_utf8(m_AnimName)); if (!m_Unit || !m_Unit->GetAnimation() || !m_Unit->GetID()) return; m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str()); } void SelectMovementAnimation(const std::string& name, fixed speed) override { ENSURE(name == "idle" || name == "walk" || name == "run"); if (m_AnimName != "idle" && m_AnimName != "walk" && m_AnimName != "run") return; if (m_AnimName == name && speed == m_AnimSpeed) return; SelectAnimation(name, false, speed); } void SetAnimationSyncRepeat(fixed repeattime) override { m_AnimSyncRepeatTime = repeattime; if (m_Unit && m_Unit->GetAnimation()) m_Unit->GetAnimation()->SetAnimationSyncRepeat(m_AnimSyncRepeatTime.ToFloat()); } void SetAnimationSyncOffset(fixed actiontime) override { m_AnimSyncOffsetTime = actiontime; if (m_Unit && m_Unit->GetAnimation()) m_Unit->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime.ToFloat()); } void SetShadingColor(fixed r, fixed g, fixed b, fixed a) override { m_R = r; m_G = g; m_B = b; UNUSED2(a); // TODO: why is this even an argument? if (m_Unit) { CModelAbstract& model = m_Unit->GetModel(); model.SetShadingColor(CColor(m_R.ToFloat(), m_G.ToFloat(), m_B.ToFloat(), 1.0f)); } } void SetVariable(const std::string& name, float value) override { if (m_Unit) m_Unit->GetModel().SetEntityVariable(name, value); } u32 GetActorSeed() const override { return m_Seed; } void SetActorSeed(u32 seed) override { if (seed == m_Seed) return; m_Seed = seed; ReloadActor(); } void RecomputeActorName() override { CmpPtr cmpValueModificationManager(GetSystemEntity()); std::wstring newActorName; if (m_IsFoundationActor) newActorName = cmpValueModificationManager->ApplyModifications(L"VisualActor/FoundationActor", m_BaseActorName, GetEntityId()); else newActorName = cmpValueModificationManager->ApplyModifications(L"VisualActor/Actor", m_BaseActorName, GetEntityId()); if (newActorName != m_ActorName) { ParseActorName(newActorName); ReloadActor(); } } bool HasConstructionPreview() const override { return m_ConstructionPreview; } void Hotload(const VfsPath& name) override { if (!m_Unit) return; if (!name.empty() && name != m_ActorName) return; ReloadActor(); } private: // Replace {phenotype} with the correct value in m_ActorName void ParseActorName(std::wstring base); /// Helper function shared by component init and actor reloading void InitModel(); /// Helper method; initializes the model selection shape descriptor from XML. Factored out for readability of @ref Init. void InitSelectionShapeDescriptor(const CParamNode& paramNode); // ReloadActor is used when the actor or seed changes. void ReloadActor(); // ReloadUnitAnimation is used for a minimal reloading upon deserialization, when the actor and seed are identical. // It is also used by ReloadActor. void ReloadUnitAnimation(); }; REGISTER_COMPONENT_TYPE(VisualActor) // ------------------------------------------------------------------------------------------------------------------ void CCmpVisualActor::ParseActorName(std::wstring base) { CmpPtr cmpIdentity(GetEntityHandle()); const std::wstring pattern = L"{phenotype}"; if (cmpIdentity) { size_t pos = base.find(pattern); while (pos != std::string::npos) { base.replace(pos, pattern.size(), cmpIdentity->GetPhenotype()); pos = base.find(pattern, pos + pattern.size()); } } m_ActorName = base; } void CCmpVisualActor::InitModel() { if (!GetSimContext().HasUnitManager()) return; std::wstring actorName = m_ActorName; if (actorName.find(L".xml") == std::wstring::npos) actorName += L".xml"; m_Unit = GetSimContext().GetUnitManager().CreateUnit(actorName, GetEntityId(), GetActorSeed()); if (!m_Unit) return; CModelAbstract& model = m_Unit->GetModel(); if (model.ToCModel()) { u32 modelFlags = 0; if (m_SilhouetteDisplay) - modelFlags |= MODELFLAG_SILHOUETTE_DISPLAY; + modelFlags |= ModelFlag::SILHOUETTE_DISPLAY; if (m_SilhouetteOccluder) - modelFlags |= MODELFLAG_SILHOUETTE_OCCLUDER; + modelFlags |= ModelFlag::SILHOUETTE_OCCLUDER; CmpPtr cmpVisibility(GetEntityHandle()); if (cmpVisibility && cmpVisibility->GetAlwaysVisible()) - modelFlags |= MODELFLAG_IGNORE_LOS; + modelFlags |= ModelFlag::IGNORE_LOS; model.ToCModel()->AddFlagsRec(modelFlags); } if (m_DisableShadows) { if (model.ToCModel()) model.ToCModel()->RemoveShadowsRec(); else if (model.ToCModelDecal()) model.ToCModelDecal()->RemoveShadows(); } bool floating = m_Unit->GetObject().m_Base->m_Properties.m_FloatOnWater; CmpPtr cmpPosition(GetEntityHandle()); if (cmpPosition) cmpPosition->SetActorFloating(floating); if (!m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); if (cmpModelRenderer) { // TODO: this should account for all possible props, animations, etc, // else we might accidentally cull the unit when it should be visible CBoundingBoxAligned bounds = m_Unit->GetModel().GetWorldBoundsRec(); CBoundingSphere boundSphere = CBoundingSphere::FromSweptBox(bounds); int flags = 0; if (m_IsActorOnly) flags |= ICmpUnitRenderer::ACTOR_ONLY; if (m_VisibleInAtlasOnly) flags |= ICmpUnitRenderer::VISIBLE_IN_ATLAS_ONLY; m_ModelTag = cmpModelRenderer->AddUnit(GetEntityHandle(), m_Unit, boundSphere, flags); } } // the model is now responsible for cleaning up the descriptor if (m_ShapeDescriptor != nullptr) m_Unit->GetModel().SetCustomSelectionShape(m_ShapeDescriptor); } void CCmpVisualActor::InitSelectionShapeDescriptor(const CParamNode& paramNode) { // by default, we don't need a custom selection shape and we can just keep the default behaviour m_ShapeDescriptor = nullptr; const CParamNode& shapeNode = paramNode.GetChild("SelectionShape"); if (shapeNode.IsOk()) { if (shapeNode.GetChild("Bounds").IsOk()) { // default; no need to take action } else if (shapeNode.GetChild("Footprint").IsOk()) { CmpPtr cmpFootprint(GetEntityHandle()); if (cmpFootprint) { ICmpFootprint::EShape fpShape; // fp stands for "footprint" entity_pos_t fpSize0, fpSize1, fpHeight; // fp stands for "footprint" cmpFootprint->GetShape(fpShape, fpSize0, fpSize1, fpHeight); float size0 = fpSize0.ToFloat(); float size1 = fpSize1.ToFloat(); // TODO: we should properly distinguish between CIRCLE and SQUARE footprint shapes here, but since cylinders // aren't implemented yet and are almost indistinguishable from boxes for small enough sizes anyway, // we'll just use boxes for either case. However, for circular footprints the size0 and size1 values both // represent the radius, so we do have to adjust them to match the size1 and size0's of square footprints // (which represent the full width and depth). if (fpShape == ICmpFootprint::CIRCLE) { size0 *= 2; size1 *= 2; } m_ShapeDescriptor = new CModelAbstract::CustomSelectionShape; m_ShapeDescriptor->m_Type = CModelAbstract::CustomSelectionShape::BOX; m_ShapeDescriptor->m_Size0 = size0; m_ShapeDescriptor->m_Size1 = size1; m_ShapeDescriptor->m_Height = fpHeight.ToFloat(); } else { LOGERROR("[VisualActor] Cannot apply footprint-based SelectionShape; Footprint component not initialized."); } } else if (shapeNode.GetChild("Box").IsOk()) { // TODO: we might need to support the ability to specify a different box center in the future m_ShapeDescriptor = new CModelAbstract::CustomSelectionShape; m_ShapeDescriptor->m_Type = CModelAbstract::CustomSelectionShape::BOX; m_ShapeDescriptor->m_Size0 = shapeNode.GetChild("Box").GetChild("@width").ToFixed().ToFloat(); m_ShapeDescriptor->m_Size1 = shapeNode.GetChild("Box").GetChild("@depth").ToFixed().ToFloat(); m_ShapeDescriptor->m_Height = shapeNode.GetChild("Box").GetChild("@height").ToFixed().ToFloat(); } else if (shapeNode.GetChild("Cylinder").IsOk()) { LOGWARNING("[VisualActor] TODO: Cylinder selection shapes are not yet implemented; defaulting to recursive bounding boxes"); } else { // shouldn't happen by virtue of validation against schema LOGERROR("[VisualActor] No selection shape specified"); } } } void CCmpVisualActor::ReloadActor() { if (!m_Unit) return; // Save some data from the old unit CColor shading = m_Unit->GetModel().GetShadingColor(); player_id_t playerID = m_Unit->GetModel().GetPlayerID(); // Replace with the new unit GetSimContext().GetUnitManager().DeleteUnit(m_Unit); // HACK: selection shape needs template data, but rather than storing all that data // in the component, we load the template here and pass it into a helper function CmpPtr cmpTemplateManager(GetSystemEntity()); const CParamNode* node = cmpTemplateManager->LoadLatestTemplate(GetEntityId()); ENSURE(node && node->GetChild("VisualActor").IsOk()); InitSelectionShapeDescriptor(node->GetChild("VisualActor")); InitModel(); if (!m_Unit) { if (m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); if (cmpModelRenderer) cmpModelRenderer->RemoveUnit(m_ModelTag); m_ModelTag = ICmpUnitRenderer::tag_t{}; } return; } ReloadUnitAnimation(); m_Unit->GetModel().SetShadingColor(shading); m_Unit->GetModel().SetPlayerID(playerID); if (m_ModelTag.valid()) { CmpPtr cmpModelRenderer(GetSystemEntity()); CBoundingBoxAligned bounds = m_Unit->GetModel().GetWorldBoundsRec(); CBoundingSphere boundSphere = CBoundingSphere::FromSweptBox(bounds); cmpModelRenderer->UpdateUnit(m_ModelTag, m_Unit, boundSphere); } } void CCmpVisualActor::ReloadUnitAnimation() { if (!m_Unit) return; m_Unit->SetEntitySelection(m_VariantSelections); if (!m_Unit->GetAnimation()) return; m_Unit->GetAnimation()->SetAnimationState(m_AnimName, m_AnimOnce, m_AnimSpeed.ToFloat(), m_AnimDesync.ToFloat(), m_SoundGroup.c_str()); // We'll lose the exact synchronisation but we should at least make sure it's going at the correct rate if (!m_AnimSyncRepeatTime.IsZero()) m_Unit->GetAnimation()->SetAnimationSyncRepeat(m_AnimSyncRepeatTime.ToFloat()); if (!m_AnimSyncOffsetTime.IsZero()) m_Unit->GetAnimation()->SetAnimationSyncOffset(m_AnimSyncOffsetTime.ToFloat()); }