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());
}