Index: source/graphics/Decal.h =================================================================== --- source/graphics/Decal.h +++ source/graphics/Decal.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -71,8 +71,6 @@ virtual void SetTerrainDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1); virtual void CalcBounds(); - virtual void ValidatePosition(); - virtual void InvalidatePosition(); virtual void SetTransform(const CMatrix3D& transform); // remove shadow receiving Index: source/graphics/Decal.cpp =================================================================== --- source/graphics/Decal.cpp +++ source/graphics/Decal.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -26,6 +26,7 @@ CModelAbstract* CModelDecal::Clone() const { CModelDecal* clone = new CModelDecal(m_Terrain, m_Decal); + CModelAbstract::Clone(*clone); return clone; } @@ -70,33 +71,6 @@ SetDirty(RENDERDATA_UPDATE_VERTICES); } -void CModelDecal::InvalidatePosition() -{ - m_PositionValid = false; -} - -void CModelDecal::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; - } - - m_PositionValid = true; -} - void CModelDecal::SetTransform(const CMatrix3D& transform) { // Since decals are assumed to be horizontal and projected downwards @@ -106,7 +80,7 @@ newTransform.Translate(transform.GetTranslation()); CRenderableObject::SetTransform(newTransform); - InvalidatePosition(); + InvalidatePositionRec(); } void CModelDecal::RemoveShadows() Index: source/graphics/Model.h =================================================================== --- source/graphics/Model.h +++ source/graphics/Model.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -28,8 +28,6 @@ #include -struct SPropPoint; -class CObjectEntry; class CSkeletonAnim; class CSkeletonAnimDef; class CSkeletonAnimManager; @@ -47,34 +45,6 @@ class CModel : public CModelAbstract { NONCOPYABLE(CModel); - -public: - struct Prop - { - Prop() : m_MinHeight(0.f), m_MaxHeight(0.f), m_Point(0), m_Model(0), m_ObjectEntry(0), m_Hidden(false), m_Selectable(true) {} - - float m_MinHeight; - float m_MaxHeight; - - /** - * 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; - - /** - * 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 - */ - CModelAbstract* m_Model; - CObjectEntry* m_ObjectEntry; - - bool m_Hidden; ///< Should this prop be temporarily removed from rendering? - bool m_Selectable; /// < should this prop count in the selection size? - }; - public: // constructor CModel(CSkeletonAnimManager& skeletonAnimManager, CSimulation2& simulation); @@ -119,47 +89,20 @@ void SetFlags(int flags) { m_Flags=flags; } // get object flags int GetFlags() const { return m_Flags; } - // add object flags, recursively through props + // Add object flags, recursively through props. + // TODO: this stops if it encounters a non-CModel prop. void AddFlagsRec(int flags); - // remove shadow casting and receiving, recursively through props - // TODO: replace with more generic shader define + flags setting + // Remove shadow casting and receiving, recursively through props + // TODO: replace with more generic shader define + flags setting. + // TODO: this stops if it encounters a non-CModel prop. void RemoveShadowsRec(); - // recurse down tree setting dirty bits - virtual void SetDirtyRec(int dirtyflags) { - SetDirty(dirtyflags); - for (size_t i=0;iSetDirtyRec(dirtyflags); - } - } - - virtual void SetTerrainDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1) - { - for (size_t i = 0; i < m_Props.size(); ++i) - m_Props[i].m_Model->SetTerrainDirty(i0, j0, i1, j1); - } - - virtual void SetEntityVariable(const std::string& name, float value) - { - 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. virtual void CalcBounds(); - /// 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; - } - - virtual const CBoundingBoxAligned GetWorldBoundsRec(); // 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. @@ -170,11 +113,6 @@ /// 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. - virtual const CBoundingBoxAligned GetObjectSelectionBoundsRec(); - /** * Set transform of this object. * @@ -210,11 +148,6 @@ */ CSkeletonAnim* BuildAnimation(const VfsPath& pathname, const CStr& name, const CStr& ID, int frequency, float speed, float actionpos, float actionpos2, float soundpos); - /** - * Add a prop to the model on the given point. - */ - void AddProp(const SPropPoint* point, CModelAbstract* 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. @@ -236,25 +169,15 @@ */ 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 virtual CModelAbstract* Clone() const; /** * Ensure that both the transformation and the bone - * matrices are correct for this model and all its props. + * matrices are correct for this model. */ virtual void ValidatePosition(); - /** - * Mark this model's position and bone matrices, - * and all props' positions as invalid. - */ - virtual void InvalidatePosition(); - private: // delete anything allocated by the model void ReleaseData(); @@ -268,10 +191,6 @@ CMaterial m_Material; // pointer to the model's raw 3d data 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; // time (in MS) into the current animation @@ -279,15 +198,8 @@ /** * 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; - // list of current props on model - std::vector m_Props; /** * The prop point to which the ammo prop is attached, or NULL if none Index: source/graphics/Model.cpp =================================================================== --- source/graphics/Model.cpp +++ source/graphics/Model.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -137,67 +137,6 @@ m_pModelDef->GetMaxBounds(anim, !(m_Flags & MODELFLAG_NOLOOPANIMATION), 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; -} - ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // BuildAnimation: load raw animation frame animation from given file, and build a // animation specific to this model @@ -249,17 +188,7 @@ 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(); + InvalidatePositionRec(); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -272,23 +201,21 @@ 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. + // Don't update recursively, we only care about the parent. + if (m_Parent) { - // Make sure we don't base our calculations on - // a parent animation state that is out of date. - m_Parent->ValidatePosition(); + if (!m_Parent->m_PositionValid) + m_Parent->ValidatePosition(); + ENSURE(m_Parent->m_PositionValid); - // Parent will recursively call our validation. - ENSURE(m_PositionValid); - return; + SetTransform(m_Parent->GetTransform() * m_ParentRelativeTransform); } 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)); } else if (m_BoneMatrices) @@ -304,28 +231,8 @@ } } - // 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; + // Our transform is up to date, bone matrices are ready: + // time to update our prop matrices. CMatrix3D translate; CVector3D objTranslation = m_Transform.GetTranslation(); @@ -344,43 +251,52 @@ objectHeight = waterHeight; } - // re-position and validate all props - for (const Prop& prop : m_Props) + for (size_t i = 0; i < m_Props.size(); ++i) { - CMatrix3D proptransform = prop.m_Point->m_Transform; + const Prop& prop = m_Props[i]; + CMatrix3D propMatrix = 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); - } + propMatrix.Concatenate(m_BoneMatrices[prop.m_Point->m_BoneIndex]); // 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 CVector3D& propTranslation = propMatrix.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); + propMatrix.Concatenate(translate); } - prop.m_Model->SetTransform(proptransform); - prop.m_Model->ValidatePosition(); + prop.m_Model->SetParentRelativeTransform(propMatrix); } + + // 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); + } + + // Vertices are in world space, joint transforms expect joint-local data, so we'll need + // to transform the world space into joint-local space first. Thus we apply the 'inverse bind bone matrix'. 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, @@ -393,8 +309,9 @@ if (computeBlendMatrices) m_pModelDef->BlendBoneMatrices(m_BoneMatrices); } -} + m_PositionValid = true; +} ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // SetAnimation: set the given animation as the current animation on this model; @@ -448,26 +365,6 @@ InvalidateBounds(); } -///////////////////////////////////////////////////////////////////////////////////////////////////////////// -// AddProp: add a prop to the model on the given point -void CModel::AddProp(const SPropPoint* point, CModelAbstract* 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 = model; - prop.m_ObjectEntry = objectentry; - prop.m_MinHeight = minHeight; - prop.m_MaxHeight = maxHeight; - prop.m_Selectable = selectable; - m_Props.push_back(prop); -} - void CModel::AddAmmoProp(const SPropPoint* point, CModelAbstract* model, CObjectEntry* objectentry) { AddProp(point, model, objectentry); @@ -559,7 +456,7 @@ { // call base class to set transform on this object CRenderableObject::SetTransform(transform); - InvalidatePosition(); + InvalidatePositionRec(); } ////////////////////////////////////////////////////////////////////////// Index: source/graphics/ModelAbstract.h =================================================================== --- source/graphics/ModelAbstract.h +++ source/graphics/ModelAbstract.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -27,19 +27,21 @@ class CModel; class CModelDecal; class CModelParticleEmitter; +class CObjectEntry; +struct SPropPoint; -/** - * 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 +#include +#include + +class CSelectableObject : public CRenderableObject { - NONCOPYABLE(CModelAbstract); + NONCOPYABLE(CSelectableObject); public: + CSelectableObject() = default; + virtual ~CSelectableObject() = default; + /** * Describes a custom selection shape to be used for a model's selection box instead of the default * recursive bounding boxes. @@ -59,63 +61,20 @@ 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) - { } - - ~CModelAbstract() - { - delete m_CustomSelectionShape; // allocated and set externally by CCmpVisualActor, but our responsibility to clean up - } - - virtual CModelAbstract* 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) - - /// Calls SetDirty on this model and all child objects. - virtual void SetDirtyRec(int dirtyflags) = 0; - - /// 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) + if (m_CustomSelectionShape.get() != descriptor) { - m_CustomSelectionShape = descriptor; + m_CustomSelectionShape = std::unique_ptr(descriptor); m_SelectionBoxValid = false; // update the selection box when it is next requested } } @@ -126,32 +85,210 @@ * 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. + * If you wish your model type to be included in selection boxes, override GetObjectsBounds. */ - virtual const CBoundingBoxAligned GetObjectSelectionBoundsRec() { return CBoundingBoxAligned::EMPTY; } + virtual const CBoundingBoxAligned GetObjectBounds() + { + RecalculateBoundsIfNecessary(); // Should recalculate both object-space and world-space bounds if necessary. + return m_ObjectBounds; + } + + 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; + } + +protected: + void CalcSelectionBox(); + + // 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; + + /// 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 + std::unique_ptr m_CustomSelectionShape; + +}; + + +/** + * 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 CSelectableObject +{ + NONCOPYABLE(CModelAbstract); + +public: + struct Prop + { + Prop() : m_MinHeight(0.f), m_MaxHeight(0.f), m_Point(0), m_Model(0), m_ObjectEntry(0), m_Hidden(false), m_Selectable(true) {} + + float m_MinHeight; + float m_MaxHeight; + + /** + * 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; + + /** + * 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 + */ + CModelAbstract* m_Model; + CObjectEntry* m_ObjectEntry; + + bool m_Hidden; ///< Should this prop be temporarily removed from rendering? + bool m_Selectable; /// < should this prop count in the selection size? + }; + +public: + + CModelAbstract() : m_Parent(NULL), m_PositionValid(false), m_ShadingColor(1, 1, 1, 1), m_PlayerID(INVALID_PLAYER) + { } + + ~CModelAbstract() + { + } + + // See below for the common implementation. + virtual CModelAbstract* 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) + + /** + * Add a prop to the model on the given point. + */ + void AddProp(const SPropPoint* point, CModelAbstract* model, CObjectEntry* objectentry, float minHeight = 0.f, float maxHeight = 0.f, bool selectable = true); + + // return prop list + std::vector& GetProps() { return m_Props; } + const std::vector& GetProps() const { return m_Props; } + + /** + * @see CSelectableObject::GetObjectBounds + */ + virtual const CBoundingBoxAligned GetObjectBounds(); + + /** + * @return world space bounds of this object and all child objects. + */ + virtual const CBoundingBoxAligned GetWorldBoundsRec() + { + CBoundingBoxAligned bounds = GetWorldBounds(); + for (const Prop& prop : m_Props) + bounds += prop.m_Model->GetWorldBoundsRec(); + return bounds; + } + + /** + * Calls SetDirty on this model and all child objects. + */ + virtual void SetDirtyRec(int dirtyflags) + { + SetDirty(dirtyflags); + for (Prop& prop : m_Props) + prop.m_Model->SetDirtyRec(dirtyflags); + } /** * 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; + virtual void SetTerrainDirty(ssize_t i0, ssize_t j0, ssize_t i1, ssize_t j1) + { + for (Prop& prop : m_Props) + prop.m_Model->SetTerrainDirty(i0, j0, i1, j1); + } /** * 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)) { } + virtual void SetEntityVariable(const std::string& name, float value) + { + for (Prop& prop : m_Props) + prop.m_Model->SetEntityVariable(name, value); + } - /** - * Ensure that both the transformation and the bone matrices are correct for this model and all its props. - */ - virtual void ValidatePosition() = 0; + void SetParentRelativeTransform(const CMatrix3D& matrix) + { + m_ParentRelativeTransform = matrix; + InvalidatePositionRec(); + } /** - * Mark this model's position and bone matrices, and all props' positions as invalid. + * Ensure that both the transformation matrices are correct for this model and all its props. */ - virtual void InvalidatePosition() = 0; + virtual void ValidatePosition() + { + if (m_PositionValid) + { + ENSURE(!m_Parent || m_Parent->m_PositionValid); + return; + } + + // Make sure we don't base our calculations on + // a parent animation state that is out of date. + // Don't update recursively, we only care about the parent. + if (m_Parent) + { + if (!m_Parent->m_PositionValid) + m_Parent->ValidatePosition(); + ENSURE(m_Parent->m_PositionValid); + + SetTransform(m_ParentRelativeTransform * m_Parent->m_Transform); + } + + m_PositionValid = true; + } + + void ValidatePositionRec() + { + ValidatePosition(); + for (Prop& prop : m_Props) + prop.m_Model->ValidatePositionRec(); + } + + void InvalidatePositionRec() + { + InvalidatePosition(); + for (Prop& prop : m_Props) + prop.m_Model->InvalidatePositionRec(); + } virtual void SetPlayerID(player_id_t id) { m_PlayerID = id; } @@ -162,7 +299,18 @@ virtual CColor GetShadingColor() const { return m_ShadingColor; } protected: - void CalcSelectionBox(); + /** + * Clone props & other generic data. + */ + virtual void Clone(CModelAbstract& o) const; + + /** + * Mark this model's position and bone matrices, and all props' positions as invalid. + */ + virtual void InvalidatePosition() + { + m_PositionValid = false; + } public: /// If non-null, points to the model that we are attached to. @@ -177,19 +325,17 @@ 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; - + // list of current props on model + std::vector m_Props; + + /** + * Transformation relative to our parent, if any. Updated by the parent. + * 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_ParentRelativeTransform; }; #endif // INCLUDED_MODELABSTRACT Index: source/graphics/ModelAbstract.cpp =================================================================== --- source/graphics/ModelAbstract.cpp +++ source/graphics/ModelAbstract.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2011 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -19,9 +19,84 @@ #include "ModelAbstract.h" +#include "graphics/PropPoint.h" #include "ps/CLogger.h" -const CBoundingBoxOriented& CModelAbstract::GetSelectionBox() +void CModelAbstract::Clone(CModelAbstract& o) const +{ + for (const Prop& prop : m_Props) + o.AddProp(prop.m_Point, prop.m_Model->Clone(), prop.m_ObjectEntry, prop.m_MinHeight, prop.m_MaxHeight, prop.m_Selectable); +} + +void CModelAbstract::AddProp(const SPropPoint* point, CModelAbstract* model, CObjectEntry* objectentry, float minHeight, float maxHeight, bool selectable) +{ + // this next call will invalidate the bounds of "model", which will in turn also invalidate the selection box + model->m_Parent = this; + model->SetParentRelativeTransform(point->m_Transform); + + Prop prop; + prop.m_Point = point; + prop.m_Model = model; + prop.m_ObjectEntry = objectentry; + prop.m_MinHeight = minHeight; + prop.m_MaxHeight = maxHeight; + prop.m_Selectable = selectable; + m_Props.push_back(prop); +} + +const CBoundingBoxAligned CModelAbstract::GetObjectBounds() +{ + CBoundingBoxAligned objBounds = CSelectableObject::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->GetObjectBounds(); + 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->m_ParentRelativeTransform; // 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; +} + +const CBoundingBoxOriented& CSelectableObject::GetSelectionBox() { if (!m_SelectionBoxValid) { @@ -31,7 +106,7 @@ return m_SelectionBox; } -void CModelAbstract::CalcSelectionBox() +void CSelectableObject::CalcSelectionBox() { if (m_CustomSelectionShape) { @@ -74,7 +149,7 @@ // standard method // Get the object-space bounds that should be used to construct this model (and its children)'s selection box - CBoundingBoxAligned objBounds = GetObjectSelectionBoundsRec(); + CBoundingBoxAligned objBounds = GetObjectBounds(); if (objBounds.IsEmpty()) { m_SelectionBox.SetEmpty(); // model does not wish to participate in selection Index: source/graphics/ModelDef.h =================================================================== --- source/graphics/ModelDef.h +++ source/graphics/ModelDef.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -22,6 +22,7 @@ #ifndef INCLUDED_MODELDEF #define INCLUDED_MODELDEF +#include "graphics/PropPoint.h" #include "maths/BoundingBoxAligned.h" #include "maths/Matrix3D.h" #include "maths/Quaternion.h" @@ -39,52 +40,6 @@ class CBoneState; class CSkeletonAnimDef; -/** - * Describes the position of a prop point within its parent model. A prop point is the location within a parent model - * where the prop's origin will be attached. - * - * A prop point is specified by its transformation matrix (or separately by its position and rotation), which - * can be relative to either the parent model's origin, or one of the parent's bones. If the parent model is boned, - * then the @ref m_BoneIndex field may specify a bone to which the transformation matrix is relative (see - * @ref CModel::m_BoneMatrices). Otherwise, the transformation matrix is assumed to be relative to the parent model's - * origin. - * - * @see CModel::m_BoneMatrices - */ -struct SPropPoint -{ - /// Name of the prop point - CStr m_Name; - - /** - * Position of the point within the parent model, relative to either the parent model's origin or one of the parent - * model's bones if applicable. Also specified as part of @ref m_Transform. - * @see m_Transform - */ - CVector3D m_Position; - - /** - * Rotation of the prop model that will be attached at this point. Also specified as part of @ref m_Transform. - * @see m_Transform - */ - CQuaternion m_Rotation; - - /** - * Object to parent space transformation. Combines both @ref m_Position and @ref m_Rotation in a single - * transformation matrix. This transformation is relative to either the parent model's origin, or one of its - * bones, depending on whether it is skeletal. If relative to a bone, then the bone in the parent model to - * which this transformation is relative may be found by m_BoneIndex. - * @see m_Position, m_Rotation - */ - CMatrix3D m_Transform; - - /** - * Index of parent bone to which this prop point is relative, if any. The value 0xFF specifies that either the parent - * model is unboned, or that this prop point is relative to the parent model's origin rather than one if its bones. - */ - u8 m_BoneIndex; -}; - /////////////////////////////////////////////////////////////////////////////// // SVertexBlend: structure containing the necessary data for blending vertices // with multiple bones Index: source/graphics/ParticleEmitter.h =================================================================== --- source/graphics/ParticleEmitter.h +++ source/graphics/ParticleEmitter.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -198,8 +198,7 @@ virtual void SetEntityVariable(const std::string& name, float value); virtual void CalcBounds(); - virtual void ValidatePosition(); - virtual void InvalidatePosition(); + virtual void UpdatePositionTransforms(); virtual void SetTransform(const CMatrix3D& transform); CParticleEmitterTypePtr m_Type; Index: source/graphics/ParticleEmitter.cpp =================================================================== --- source/graphics/ParticleEmitter.cpp +++ source/graphics/ParticleEmitter.cpp @@ -261,7 +261,9 @@ CModelAbstract* CModelParticleEmitter::Clone() const { - return new CModelParticleEmitter(m_Type); + CModelAbstract* clone = new CModelParticleEmitter(m_Type); + CModelAbstract::Clone(*clone); + return clone; } void CModelParticleEmitter::CalcBounds() @@ -273,7 +275,7 @@ m_WorldBounds = m_Type->CalculateBounds(m_Emitter->GetPosition(), m_Emitter->GetParticleBounds()); } -void CModelParticleEmitter::ValidatePosition() +void CModelParticleEmitter::UpdatePositionTransforms() { // TODO: do we need to do anything here? @@ -282,10 +284,6 @@ InvalidateBounds(); } -void CModelParticleEmitter::InvalidatePosition() -{ -} - void CModelParticleEmitter::SetTransform(const CMatrix3D& transform) { if (m_Transform == transform) Index: source/graphics/PropPoint.h =================================================================== --- /dev/null +++ source/graphics/PropPoint.h @@ -0,0 +1,72 @@ +/* Copyright (C) 2022 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_PROPPOINT +#define INCLUDED_PROPPOINT + +#include "maths/Matrix3D.h" +#include "maths/Quaternion.h" +#include "maths/Vector3D.h" +#include "ps/CStr.h" + +/** + * Describes the position of a prop point within its parent. + * A prop point is the location within a parent where the prop's origin will be attached. + * + * A prop point is specified by its transformation matrix (or separately by its position and rotation), which + * can be relative to either the parent's origin, or one of the parent's bones. If the parent is boned, + * then the @ref m_BoneIndex field may specify a bone to which the transformation matrix is relative + * (see @ref CModel::m_BoneMatrices). + * Otherwise, the transformation matrix is assumed to be relative to the parent's origin. + * + * @see CModel::m_BoneMatrices + */ +struct SPropPoint +{ + CStr m_Name; + + /** + * Position of the point within the parent model, relative to either the parent's origin, + * or one of the parent's bones if applicable. + * Also specified as part of @ref m_Transform. + * @see m_Transform + */ + CVector3D m_Position; + + /** + * Rotation of the prop that will be attached at this point. Also specified as part of @ref m_Transform. + * @see m_Transform + */ + CQuaternion m_Rotation; + + /** + * Object to parent space transformation. Combines both @ref m_Position and @ref m_Rotation in a single + * transformation matrix. This transformation is relative to either the parent's origin, or one of its + * bones, depending on whether it is skeletal. If relative to a bone, then the bone in the parent model to + * which this transformation is relative may be found by m_BoneIndex. + * @see m_Position, m_Rotation + */ + CMatrix3D m_Transform; + + /** + * Index of parent bone to which this prop point is relative, if any. The value 0xFF specifies that either the parent + * model is unboned, or that this prop point is relative to the parent model's origin rather than one if its bones. + */ + u8 m_BoneIndex; +}; + +#endif // INCLUDED_PROPPOINT Index: source/simulation2/components/CCmpVisualActor.cpp =================================================================== --- source/simulation2/components/CCmpVisualActor.cpp +++ source/simulation2/components/CCmpVisualActor.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2022 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -407,7 +407,7 @@ float frameOffset = cmpUnitRenderer->GetFrameOffset(); CMatrix3D transform(cmpPosition->GetInterpolatedTransform(frameOffset)); m_Unit->GetModel().SetTransform(transform); - m_Unit->GetModel().ValidatePosition(); + m_Unit->GetModel().ValidatePositionRec(); } CModelAbstract* ammo = m_Unit->GetModel().ToCModel()->FindFirstAmmoProp();