Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -131,6 +131,10 @@ ; Quality used for actors. max_actor_quality=200 +; Distance range over which to apply LOD. +lod.near = 100 +lod.far = 400 + ; Quality level of shader effects (set to 10 to display all effects) materialmgr.quality = 2.0 Index: binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e.xml =================================================================== --- binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e.xml +++ binaries/data/mods/public/art/actors/units/athenians/infantry_spearman_e.xml @@ -1,215 +1,240 @@ - - - - - skeletal/new/m_armor_tunic_short.dae - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - player_trans_spec.xml - + + + + + skeletal/new/m_armor_tunic_short.dae + + + + + + + + + player_trans.xml + + + + + + + + + skeletal/new/m_armor_tunic_short.dae + + + + + + skeletal/new/m_armor_tunic_short.dae + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + player_trans_spec.xml + + Index: binaries/data/mods/public/art/materials/default.xml =================================================================== --- binaries/data/mods/public/art/materials/default.xml +++ binaries/data/mods/public/art/materials/default.xml @@ -1,4 +1,4 @@ - + Index: source/graphics/ObjectBase.h =================================================================== --- source/graphics/ObjectBase.h +++ source/graphics/ObjectBase.h @@ -236,6 +236,11 @@ */ const std::shared_ptr& GetBase(u8 QualityLevel) const; + /** + * Return the iterator for the base matching the given quality level. + */ + std::vector>::const_iterator GetBaseIterator(u8 QualityLevel) const; + /** * Initialise this object by loading from the given file. * Returns false on error. Index: source/graphics/ObjectBase.cpp =================================================================== --- source/graphics/ObjectBase.cpp +++ source/graphics/ObjectBase.cpp @@ -761,16 +761,22 @@ const std::shared_ptr& CActorDef::GetBase(u8 QualityLevel) const { - for (const std::shared_ptr& base : m_ObjectBases) - if (base->m_QualityLevel >= QualityLevel) - return base; + // Must return something once the actor is initialised, so dereferencing is safe. + return *GetBaseIterator(QualityLevel); +} + +std::vector>::const_iterator CActorDef::GetBaseIterator(u8 QualityLevel) const +{ + for (std::vector>::const_iterator it = m_ObjectBases.begin(); it != m_ObjectBases.end(); ++it) + if ((*it)->m_QualityLevel >= QualityLevel) + return it; // This code path ought to be impossible to take, // because by construction we must have at least one valid CObjectBase of quality MAX_QUALITY // (which necessarily fits the u8 comparison above). // However compilers will warn that we return a reference to a local temporary if I return nullptr, // so just return something sane instead. ENSURE(false); - return m_ObjectBases.back(); + return m_ObjectBases.end(); } bool CActorDef::Load(const VfsPath& pathname) Index: source/graphics/ObjectManager.h =================================================================== --- source/graphics/ObjectManager.h +++ source/graphics/ObjectManager.h @@ -77,19 +77,20 @@ /** * Get the object entry for a given actor & the given selections list. - * @param selections - a possibly incomplete list of selections. - * @param seed - the randomness seed to use to complete the random selections. - */ - CObjectEntry* FindObjectVariation(const CActorDef* actor, const std::vector>& selections, uint32_t seed); - - /** - * @see FindObjectVariation. - * These take a complete selection. These are pointers to sets that are + * Takes a "complete selection set". These are pointers to sets that are * guaranteed to exist (pointers are used to avoid copying the sets). */ CObjectEntry* FindObjectVariation(const std::shared_ptr& base, const std::vector*>& completeSelections); CObjectEntry* FindObjectVariation(const CStrW& objname, const std::vector*>& completeSelections); + /** + * Return an object entry for each quality level of this actor. Random variations will be shared where possible. + * @param selections - a possibly incomplete list of selections. + * @param seed - the randomness seed to use to complete the random selections. + * @see FindObjectVariation + */ + std::vector FindObjectVariations(const CActorDef& actor, const std::vector>& selections, uint32_t seed); + /** * Get the terrain object that actors managed by this manager should be linked * with (primarily for the purpose of decals) Index: source/graphics/ObjectManager.cpp =================================================================== --- source/graphics/ObjectManager.cpp +++ source/graphics/ObjectManager.cpp @@ -92,24 +92,40 @@ return { success, *m_ActorDefs.insert_or_assign(actorName, std::move(actor)).first->second.obj }; } -CObjectEntry* CObjectManager::FindObjectVariation(const CActorDef* actor, const std::vector>& selections, uint32_t seed) +std::vector CObjectManager::FindObjectVariations(const CActorDef& actor, const std::vector>& selections, uint32_t seed) { - if (!actor) - return nullptr; - - const std::shared_ptr& base = actor->GetBase(m_QualityLevel); + std::vector ret; std::vector*> completeSelections; for (const std::set& selectionSet : selections) completeSelections.emplace_back(&selectionSet); + + + std::vector>::const_iterator it = actor.m_ObjectBases.begin(); + // Must exist. + ENSURE(actor.m_ObjectBases.size() > 0); + std::vector>::const_iterator highestQual = --actor.m_ObjectBases.end(); // To maintain a consistent look between quality levels, first complete with the highest-quality variants. // then complete again at the required quality level (since not all variants may be available). - std::set highQualitySelections = actor->GetBase(255)->CalculateRandomRemainingSelections(seed, selections); + const std::shared_ptr& highQualityBase = *highestQual; + std::set highQualitySelections = highQualityBase->CalculateRandomRemainingSelections(seed, selections); completeSelections.emplace_back(&highQualitySelections); - // We don't have to pass the high-quality selections here because they have higher priority anyways. - std::set remainingSelections = base->CalculateRandomRemainingSelections(seed, selections); - completeSelections.emplace_back(&remainingSelections); - return FindObjectVariation(base, completeSelections); + + std::vector>::const_iterator maxQuality = actor.GetBaseIterator(m_QualityLevel); + + while (true) + { + const std::shared_ptr& base = *it; + std::set remainingSelections = base->CalculateRandomRemainingSelections(seed, selections); + completeSelections.emplace_back(&remainingSelections); + CObjectEntry* entry = FindObjectVariation(base, completeSelections); + if (entry) + ret.push_back(entry); + completeSelections.pop_back(); + if (it++ == maxQuality) + break; + } + return ret; } CObjectEntry* CObjectManager::FindObjectVariation(const std::shared_ptr& base, const std::vector*>& completeSelections) Index: source/graphics/Unit.h =================================================================== --- source/graphics/Unit.h +++ source/graphics/Unit.h @@ -18,12 +18,15 @@ #ifndef INCLUDED_UNIT #define INCLUDED_UNIT -#include -#include #include "ps/CStr.h" #include "simulation2/system/Entity.h" // entity_id_t +#include +#include +#include +#include + class CActorDef; class CModelAbstract; class CObjectEntry; @@ -54,9 +57,9 @@ // get unit's template object const CObjectEntry& GetObject() const { return *m_Object; } // get unit's model data - CModelAbstract& GetModel() const { return *m_Model; } + CModelAbstract& GetModel() const { return *m_Model->get(); } - CUnitAnimation* GetAnimation() { return m_Animation; } + CUnitAnimation* GetAnimation() { return m_Animation.get(); } /** * Update the model's animation. @@ -64,6 +67,8 @@ */ void UpdateModel(float frameTime); + void UpdateQualityLevel(u8 quality); + // Sets the entity-selection, and updates the unit to use the new // actor variation. Either set one key at a time, or a complete map. void SetEntitySelection(const CStr& key, const CStr& selection); @@ -82,12 +87,18 @@ private: // Actor for the unit const CActorDef& m_Actor; - // object from which unit was created; never NULL once fully created. + + // Objects at varying quality levels. Never nullptr after initialization. + std::vector m_Objects; + // Models at varying quality levels. A particular model may be nullptr if the quality level has not yet been used. + std::vector> m_Models; + + // Current object entry, never nullptr after initialization. CObjectEntry* m_Object = nullptr; - // object model representation; never NULL once fully created. - CModelAbstract* m_Model = nullptr; + // Current model. Always point to a real element after initialization. + std::vector>::iterator m_Model; - CUnitAnimation* m_Animation = nullptr; + std::unique_ptr m_Animation; // unique (per map) ID number for units created in the editor, as a // permanent way of referencing them. @@ -97,15 +108,22 @@ uint32_t m_Seed; // actor-level selections for this unit + // TODO: this is actually completely useless at the moment, it should be deleted or turned into something useful. std::set m_ActorSelections; // entity-level selections for this unit std::map m_EntitySelections; + u8 m_QualityLevel = 255; + // object manager which looks after this unit's objectentry CObjectManager& m_ObjectManager; void ReloadObject(); + u8 GetIndexForQuality(u8 quality); + + void UpdateCurrentModel(const std::unique_ptr& oldModel); + friend class CUnitAnimation; }; Index: source/graphics/Unit.cpp =================================================================== --- source/graphics/Unit.cpp +++ source/graphics/Unit.cpp @@ -31,12 +31,11 @@ CUnit::CUnit(CObjectManager& objectManager, const CActorDef& actor, uint32_t seed) : m_ID(INVALID_ENTITY), m_ObjectManager(objectManager), m_Actor(actor), m_Seed(seed), m_Animation(nullptr) { + m_Model = m_Models.end(); } CUnit::~CUnit() { - delete m_Animation; - delete m_Model; } CUnit* CUnit::Create(const CStrW& actorName, uint32_t seed, const std::set& selections, CObjectManager& objectManager) @@ -45,7 +44,7 @@ CUnit* unit = new CUnit(objectManager, actor, seed); unit->SetActorSelections(selections); // Calls ReloadObject(). - if (!unit->m_Model) + if (unit->m_Model == unit->m_Models.end()) { delete unit; return nullptr; @@ -59,6 +58,21 @@ m_Animation->Update(frameTime*1000.0f); } +void CUnit::UpdateQualityLevel(u8 quality) +{ + if (quality == m_QualityLevel) + return; + u8 newIndex = GetIndexForQuality(quality); + u8 oldIndex = GetIndexForQuality(m_QualityLevel); + m_QualityLevel = quality; + if (newIndex == oldIndex) + return; + // Update the current object & model and update. + m_Object = m_Objects[newIndex]; + m_Model = m_Models.begin() + newIndex; + UpdateCurrentModel(m_Models[oldIndex]); +} + void CUnit::SetID(entity_id_t id) { m_ID = id; @@ -105,54 +119,79 @@ // 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) + std::vector objects = m_ObjectManager.FindObjectVariations(m_Actor, selections, m_Seed); + + if (objects.empty()) { - LOGERROR("Error loading object variation (actor: %s)", m_Actor.GetPathname().string8()); + LOGERROR("Error loading object variations (actor: %s)", m_Actor.GetPathname().string8()); // Don't delete the unit, don't override our current (valid) state. return; } - if (!m_Object) + // If these selections give the same objects, do nothing. + if (objects == m_Objects) + return; + + // Otherwise, switch the objects, the current object, and reload. + m_Objects = objects; + + std::unique_ptr oldModel = m_Model != m_Models.end() ? std::move(*m_Model) : std::unique_ptr(); + // Re-init the models with null pointers. + m_Models.clear(); + m_Models.resize(m_Objects.size()); + + u8 i = GetIndexForQuality(m_QualityLevel); + m_Object = m_Objects[i]; + m_Model = m_Models.begin() + i; + UpdateCurrentModel(oldModel); +} + +u8 CUnit::GetIndexForQuality(u8 quality) +{ + for (size_t i = 0; i < m_Objects.size(); ++i) + if (m_Objects[i]->m_Base->m_QualityLevel >= quality) + return i; + return m_Objects.size() - 1; +} + +void CUnit::UpdateCurrentModel(const std::unique_ptr& oldModel) +{ + ENSURE(m_Model != m_Models.end()); + CModelAbstract* model = m_Model->get(); + + if (!model) { - m_Object = newObject; - m_Model = newObject->m_Model->Clone(); - if (m_Model->ToCModel()) - m_Animation = new CUnitAnimation(m_ID, m_Model->ToCModel(), m_Object); + *m_Model = std::unique_ptr(m_Object->m_Model->Clone()); + model = m_Model->get(); } - else if (m_Object && newObject != m_Object) - { - // Clone the new object's base (non-instance) model - CModelAbstract* newModel = newObject->m_Model->Clone(); + // Nothing to do if the model was already loaded and is already selected. + else if (model == oldModel.get()) + return; + // Copy data from the current model, if relevant. + if (oldModel) + { // 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()) + model->SetTransform(oldModel->GetTransform()); + model->SetPlayerID(oldModel->GetPlayerID()); + if (model->ToCModel() && oldModel->ToCModel()) { - newModel->ToCModel()->CopyAnimationFrom(m_Model->ToCModel()); + model->ToCModel()->CopyAnimationFrom(oldModel->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(); - newModel->ToCModel()->AddFlagsRec(instanceFlags); + int instanceFlags = (MODELFLAG_SILHOUETTE_DISPLAY|MODELFLAG_SILHOUETTE_OCCLUDER|MODELFLAG_IGNORE_LOS) & oldModel->ToCModel()->GetFlags(); + model->ToCModel()->AddFlagsRec(instanceFlags); } + } - delete m_Model; - m_Model = 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); - } + if (model->ToCModel()) + { + if (m_Animation) + m_Animation->ReloadUnit(model->ToCModel(), m_Object); // TODO: maybe this should try to preserve animation state? else - { - SAFE_DELETE(m_Animation); - } + m_Animation = std::make_unique(m_ID, model->ToCModel(), m_Object); } + else + m_Animation.reset(); } Index: source/simulation2/components/CCmpUnitRenderer.cpp =================================================================== --- source/simulation2/components/CCmpUnitRenderer.cpp +++ source/simulation2/components/CCmpUnitRenderer.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -35,9 +35,11 @@ #include "maths/BoundingSphere.h" #include "maths/Frustum.h" #include "maths/Matrix3D.h" +#include "ps/ConfigDB.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "renderer/RenderingOptions.h" +#include "renderer/Renderer.h" #include "renderer/Scene.h" #include "tools/atlas/GameInterface/GameLoop.h" @@ -392,6 +394,13 @@ PROFILE3("UnitRenderer::RenderSubmit"); + float nearLOD, farLOD; + CFG_GET_VAL("lod.near", nearLOD); + CFG_GET_VAL("lod.far", farLOD); + CVector3D cameraOrigin = CRenderer::GetSingleton().GetViewCamera().m_Orientation.GetTranslation(); + // Multiply the perceived distance by FOV - for smaller FOV, distances must be longer for the minute-of-angle delta to be the same. + float fovRatio = std::max(0.01f, CRenderer::GetSingleton().GetViewCamera().GetFOV() / 0.785398f); + for (size_t i = 0; i < m_Units.size(); ++i) { SUnit& unit = m_Units[i]; @@ -418,6 +427,13 @@ unit.culled = false; + CmpPtr cmpPosition(unit.entity); + if (!cmpPosition) + continue; + float ratio = (cameraOrigin - cmpPosition->GetPosition()).Length() * fovRatio; + ratio = 255 - 255 * (ratio - nearLOD < 0 ? 0 : ratio - nearLOD) / (farLOD-nearLOD); + unit.actor->UpdateQualityLevel(ratio <= 0.f ? 0 : (ratio >= 255.f ? 255 : (int)ratio)); + CModelAbstract& unitModel = unit.actor->GetModel(); if (unit.lastTransformFrame != m_FrameNumber)