Index: ps/trunk/source/graphics/ObjectEntry.cpp =================================================================== --- ps/trunk/source/graphics/ObjectEntry.cpp (revision 27575) +++ ps/trunk/source/graphics/ObjectEntry.cpp (revision 27576) @@ -1,316 +1,322 @@ -/* Copyright (C) 2022 Wildfire Games. +/* 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, CSimulation2& simulation) : m_Base(base), m_Color(1.0f, 1.0f, 1.0f, 1.0f), m_Model(NULL), m_Simulation(simulation) { } CObjectEntry::~CObjectEntry() { delete m_Model; } 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); - textureProps.SetAddressMode(Renderer::Backend::Sampler::AddressMode::CLAMP_TO_BORDER); + // 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 = new CModelDecal(objectManager.GetTerrain(), decal); return true; } if (!variation.particles.empty()) { m_Model = new CModelParticleEmitter(g_Renderer.GetSceneRenderer().GetParticleManager().LoadEmitterType(variation.particles)); return true; } if (variation.model.empty()) { m_Model = new CModelDummy(); 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 CModel* model = new CModel(m_Simulation); delete m_Model; m_Model = model; model->SetMaterial(g_Renderer.GetSceneRenderer().GetMaterialManager().LoadMaterial(m_Base->m_Material)); model->GetMaterial().AddStaticUniform("objectColor", CVector4D(m_Color.r, m_Color.g, m_Color.b, m_Color.a)); model->InitModel(modeldef); 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(); model->GetMaterial().AddSampler(CMaterial::TextureSampler(samp.m_SamplerName, texture)); } 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) { CModelAbstract* propmodel = oe->m_Model->Clone(); if (isAmmo) model->AddAmmoProp(proppoint, propmodel, oe); else model->AddProp(proppoint, propmodel, oe, prop.m_minHeight, prop.m_maxHeight, prop.m_selectable); if (propmodel->ToCModel()) propmodel->ToCModel()->SetAnimation(oe->GetRandomAnimation("idle")); } 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); } if (m_Base->m_Properties.m_FloatOnWater) { model->SetFlags(model->GetFlags() | MODELFLAG_FLOATONWATER); } 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; }