0ad/source/graphics/tests/test_Model.h
trompetin17 c3d3943047
Fix incorrect shadow behavior in VisualActors
This commit primarily corrects an inaccurate comment and resolves a
hidden bug in the VisualActor shadow logic, while preserving the current
behavior.

Previously, the DisableShadows option removed both casting and receiving
shadows. However, the internal logic ignored this setting in some cases,
causing shadows to appear even when explicitly disabled in XML
templates.

This change introduces the ability to control CastShadows and
ReceiveShadows independently via template settings, defaulting both to
true unless explicitly specified.

In addition, previews are updated to receive shadows but not cast them,
improving visual volume effects without affecting lighting behavior.
2025-06-03 19:30:10 -05:00

277 lines
9.3 KiB
C++

/* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#include "lib/self_test.h"
#include "graphics/ColladaManager.h"
#include "graphics/Decal.h"
#include "graphics/Material.h"
#include "graphics/Model.h"
#include "graphics/ModelDef.h"
#include "graphics/ObjectEntry.h"
#include "graphics/ObjectManager.h"
#include "graphics/ShaderDefines.h"
#include "graphics/SkeletonAnimManager.h"
#include "graphics/Terrain.h"
#include "graphics/Unit.h"
#include "graphics/UnitManager.h"
#include "lib/file/io/io.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/CStrInternStatic.h"
#include "ps/Filesystem.h"
#include "ps/ProfileViewer.h"
#include "ps/VideoMode.h"
#include "ps/XML/Xeromyces.h"
#include "renderer/backend/dummy/Device.h"
#include "renderer/Renderer.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include <optional>
#include <memory>
#include <string_view>
namespace
{
constexpr std::string_view TEST_SKELETON_XML{R"(<?xml version="1.0" encoding="UTF-8"?><skeletons></skeletons>)"};
constexpr std::string_view TEST_MESH_XML{R"(<?xml version="1.0" encoding="utf-8"?>
<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">
<asset>
<unit name="meter" meter="1"/>
<up_axis>Z_UP</up_axis>
</asset>
<library_geometries>
<geometry id="Plane-mesh" name="Plane">
<mesh>
<source id="Plane-mesh-positions">
<float_array id="Plane-mesh-positions-array" count="12">1 1 0 1 -1 0 -1 -0.9999998 0 -0.9999997 1 0</float_array>
</source>
<source id="Plane-mesh-normals">
<float_array id="Plane-mesh-normals-array" count="3">0 0 1</float_array>
</source>
<source id="Plane-mesh-map-0">
<float_array id="Plane-mesh-map-0-array" count="8">0 0 1 0 1 1 0 1</float_array>
</source>
<vertices id="Plane-mesh-vertices">
<input semantic="POSITION" source="#Plane-mesh-positions"/>
</vertices>
<polylist count="1">
<input semantic="VERTEX" source="#Plane-mesh-vertices" offset="0"/>
<input semantic="NORMAL" source="#Plane-mesh-normals" offset="1"/>
<input semantic="TEXCOORD" source="#Plane-mesh-map-0" offset="2" set="0"/>
<vcount>4 </vcount>
<p>0 0 0 3 0 1 2 0 2 1 0 3</p>
</polylist>
</mesh>
</geometry>
</library_geometries>
<library_visual_scenes>
<visual_scene id="Scene" name="Scene">
<node id="Plane" type="NODE"><instance_geometry url="#Plane-mesh"/></node>
</visual_scene>
</library_visual_scenes>
<scene><instance_visual_scene url="#Scene"/></scene>
</COLLADA>)"};
constexpr std::string_view TEST_ACTOR_WITH_SHADOWS_NAME{"test_with_shadows.xml"};
constexpr std::string_view TEST_ACTOR_WITH_SHADOWS_XML{R"(<?xml version="1.0" encoding="utf-8"?>
<actor version="1">
<castshadow/>
<group>
<variant><mesh>test.dae</mesh></variant>
</group>
</actor>)"};
}
class TestModel : public CxxTest::TestSuite
{
std::optional<CXeromycesEngine> m_XeromycesEngine;
OsPath m_ModPath;
OsPath m_CachePath;
std::unique_ptr<CProfileViewer> m_Viewer;
std::unique_ptr<CRenderer> m_Renderer;
public:
void setUp()
{
g_VFS = CreateVfs();
CConfigDB::Initialise();
CConfigDB::Instance()->SetValueString(CFG_SYSTEM, "rendererbackend", "dummy");
m_XeromycesEngine.emplace();
TestLogger logger;
g_VideoMode.InitNonSDL();
g_VideoMode.CreateBackendDevice(false);
m_Viewer = std::make_unique<CProfileViewer>();
m_Renderer = std::make_unique<CRenderer>(g_VideoMode.GetBackendDevice());
m_ModPath = DataDir() / "mods" / "_test.model" / "";
m_CachePath = DataDir() / "_testcache" / "";
const OsPath skeletonsPath = m_ModPath / "art" / "skeletons";
TS_ASSERT_EQUALS(INFO::OK, CreateDirectories(skeletonsPath, 0700, false));
const OsPath meshesPath = m_ModPath / "art" / "meshes";
TS_ASSERT_EQUALS(INFO::OK, CreateDirectories(meshesPath, 0700, false));
const OsPath testSkeletonPath = skeletonsPath / "test.xml";
TS_ASSERT_EQUALS(INFO::OK, io::Store(testSkeletonPath, TEST_SKELETON_XML.data(), TEST_SKELETON_XML.size()));
const OsPath testMeshPath = meshesPath / "test.dae";
TS_ASSERT_EQUALS(INFO::OK, io::Store(testMeshPath, TEST_MESH_XML.data(), TEST_MESH_XML.size()));
const OsPath actorsPath = m_ModPath / "art" / "actors";
TS_ASSERT_EQUALS(INFO::OK, CreateDirectories(actorsPath, 0700, false));
const OsPath testActorPath = actorsPath / TEST_ACTOR_WITH_SHADOWS_NAME.data();
TS_ASSERT_EQUALS(INFO::OK, io::Store(testActorPath, TEST_ACTOR_WITH_SHADOWS_XML.data(), TEST_ACTOR_WITH_SHADOWS_XML.size()));
TS_ASSERT_OK(g_VFS->Mount(L"", m_ModPath));
TS_ASSERT_OK(g_VFS->Mount(L"cache/", m_CachePath, 0, VFS_MAX_PRIORITY));
}
void tearDown()
{
m_Renderer.reset();
m_Viewer.reset();
g_VideoMode.Shutdown();
m_XeromycesEngine.reset();
CConfigDB::Shutdown();
g_VFS.reset();
DeleteDirectory(m_ModPath);
DeleteDirectory(m_CachePath);
}
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);
}
bool HasMaterialDefine(CModelDecal* model, CStrIntern define)
{
return HasShaderDefine(model->m_Decal.m_Material.GetShaderDefines(), define);
}
void test_model_with_flags()
{
TestLogger logger;
CMaterial material{};
CSimulation2 simulation{nullptr, *g_ScriptContext, nullptr};
CTerrain terrain;
terrain.Initialize(4, nullptr);
// TODO: load a proper mock for modeldef.
CModelDefPtr modeldef = std::make_shared<CModelDef>();
std::unique_ptr<CModel> model = std::make_unique<CModel>(simulation, material, modeldef);
SPropPoint propPoint{};
model->AddProp(&propPoint, std::make_unique<CModel>(simulation, material, modeldef), nullptr);
SDecal decal{CMaterial{}, 4.0f, 4.0f, 0.0f, 4.0f, 4.0f, false};
model->AddProp(&propPoint, std::make_unique<CModelDecal>(&terrain, decal), nullptr);
model->AddFlagsRec(ModelFlag::IGNORE_LOS);
model->RemoveShadowsCast();
model->RemoveShadowsReceive();
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() || prop.m_Model->ToCModelDecal());
if (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));
}
else if (prop.m_Model->ToCModelDecal())
{
TS_ASSERT(HasMaterialDefine(prop.m_Model->ToCModelDecal(), str_DISABLE_RECEIVE_SHADOWS));
}
}
std::unique_ptr<CModelAbstract> 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() || prop.m_Model->ToCModelDecal());
if (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));
}
else if (prop.m_Model->ToCModelDecal())
{
TS_ASSERT(HasMaterialDefine(prop.m_Model->ToCModelDecal(), str_DISABLE_RECEIVE_SHADOWS));
}
}
}
void test_unit_reload()
{
TestLogger logger;
CColladaManager colladaManager{g_VFS};
CMeshManager meshManager{colladaManager};
CSkeletonAnimManager skeletonAnimationManager{colladaManager};
CUnitManager unitManager;
CSimulation2 simulation{&unitManager, *g_ScriptContext, nullptr};
CObjectManager objectManager{
meshManager, skeletonAnimationManager, simulation};
unitManager.SetObjectManager(objectManager);
const CStrW actorName = CStr{TEST_ACTOR_WITH_SHADOWS_NAME}.FromUTF8();
const entity_id_t id = 1;
const uint32_t seed = 1;
CUnit* unit = unitManager.CreateUnit(actorName, id, seed);
TS_ASSERT(unit);
CModel* model = unit->GetModel().ToCModel();
TS_ASSERT(model);
TS_ASSERT((model->GetFlags() & ModelFlag::CAST_SHADOWS) == ModelFlag::CAST_SHADOWS);
auto [success, actor] = objectManager.FindActorDef(actorName);
TS_ASSERT(success);
const uint32_t newSeed = 2;
// Trigger the unit reload.
unit->SetActorSelections(actor.PickSelectionsAtRandom(newSeed));
model = unit->GetModel().ToCModel();
TS_ASSERT(model);
TS_ASSERT((model->GetFlags() & ModelFlag::CAST_SHADOWS) == ModelFlag::CAST_SHADOWS);
}
};