- Added new test cases for various graphics functionalities, including animation previews, particle effects, and inventory UI. - Implemented rendering previews for Mixamo animations, Box2D physics, and procedural generation, allowing for real-time visualization during tests. - Introduced helper functions for drawing graphics elements, improving the clarity and maintainability of test code. - Enhanced existing tests to utilize the new graphics capabilities, ensuring comprehensive coverage of rendering features.
400 lines
13 KiB
C++
400 lines
13 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "scene/NodeDesc.h"
|
|
#include "scene/Scene.h"
|
|
#include "scene/SceneComponent.h"
|
|
#include "scene/SceneHost.h"
|
|
#include "scene/TransformSystem.h"
|
|
|
|
#include "graphics/Graphics.h"
|
|
#include "graphics/RenderSystem.h"
|
|
#include "graphics/RenderSystem3D.h"
|
|
#include "window/Window.h"
|
|
|
|
#include <SDL2/SDL.h>
|
|
|
|
#include <cmath>
|
|
#include <glm/gtc/matrix_transform.hpp>
|
|
#include <string>
|
|
|
|
using namespace eve::scene;
|
|
using namespace eve::graphics;
|
|
|
|
namespace {
|
|
|
|
bool approxEq(float a, float b, float eps = 1e-4f) { return std::fabs(a - b) < eps; }
|
|
|
|
bool matApproxEq(const glm::mat4 &a, const glm::mat4 &b, float eps = 1e-4f) {
|
|
for (int c = 0; c < 4; ++c) {
|
|
for (int r = 0; r < 4; ++r) {
|
|
if (!approxEq(a[c][r], b[c][r], eps)) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
class BattleScene : public SceneComponent {
|
|
public:
|
|
bool showAlly = true;
|
|
NodeDesc slot;
|
|
|
|
NodeDesc build() override {
|
|
std::vector<NodeDesc> kids = {
|
|
node("map").withPosition(0.f, 0.f, 0.f),
|
|
node("player").withPosition(1.f, 0.f, 0.f),
|
|
when(showAlly, node("ally").withPosition(2.f, 0.f, 0.f)),
|
|
};
|
|
if (slot.id.empty()) kids.push_back(group({}, "__slot_empty"));
|
|
else kids.push_back(slot);
|
|
return node("battle", std::move(kids), "battle");
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Scene.NodeDesc.composeNestedTree") {
|
|
SceneHost *h = SceneHost::createHost("nest");
|
|
h->setTree(node("root",
|
|
{
|
|
node("a").withPosition(1.f, 0.f, 0.f),
|
|
group({node("b").withPosition(0.f, 2.f, 0.f)}, "g"),
|
|
}));
|
|
|
|
auto t = h->tree();
|
|
REQUIRE_EQ(t->root, 0);
|
|
REQUIRE_GE(t->nodes.size(), 4u);
|
|
CHECK(h->findById("a") != nullptr);
|
|
CHECK(h->findById("b") != nullptr);
|
|
CHECK_EQ(h->findById("root")->firstChild, 1);
|
|
}
|
|
|
|
TEST_CASE("Scene.reconcile.patchesPropsKeepsIdentity") {
|
|
SceneHost *h = SceneHost::createHost("recon");
|
|
h->setTree(node("root", {node("player").withPosition(0.f, 0.f, 0.f)}));
|
|
SceneNode *before = h->findById("player");
|
|
REQUIRE(before != nullptr);
|
|
const std::string *idPtr = &before->id;
|
|
|
|
bool rebuilt = h->setTreeReconcile(node("root", {node("player").withPosition(3.f, 4.f, 5.f)}));
|
|
CHECK(!rebuilt);
|
|
SceneNode *after = h->findById("player");
|
|
REQUIRE(after != nullptr);
|
|
CHECK_EQ(&after->id, idPtr);
|
|
CHECK(approxEq(after->x, 3.f));
|
|
CHECK(approxEq(after->y, 4.f));
|
|
CHECK(approxEq(after->z, 5.f));
|
|
}
|
|
|
|
TEST_CASE("Scene.reconcile.structureChangeRebuilds") {
|
|
SceneHost *h = SceneHost::createHost("recon2");
|
|
h->setTree(node("root", {node("a")}));
|
|
bool rebuilt = h->setTreeReconcile(node("root", {node("a"), node("b")}));
|
|
CHECK(rebuilt);
|
|
CHECK(h->findById("b") != nullptr);
|
|
}
|
|
|
|
TEST_CASE("Scene.TransformSystem.parentChildWorld") {
|
|
SceneHost *h = SceneHost::createHost("xf");
|
|
h->setTree(node("root", {node("child").withPosition(1.f, 0.f, 0.f)}).withPosition(10.f, 0.f, 0.f));
|
|
TransformSystem::updateHost(h);
|
|
|
|
SceneNode *root = h->findById("root");
|
|
SceneNode *child = h->findById("child");
|
|
REQUIRE(root != nullptr);
|
|
REQUIRE(child != nullptr);
|
|
|
|
glm::mat4 expectRoot = glm::translate(glm::mat4(1.f), glm::vec3(10.f, 0.f, 0.f));
|
|
glm::mat4 expectChild = expectRoot * glm::translate(glm::mat4(1.f), glm::vec3(1.f, 0.f, 0.f));
|
|
CHECK(matApproxEq(root->world, expectRoot));
|
|
CHECK(matApproxEq(child->world, expectChild));
|
|
}
|
|
|
|
TEST_CASE("Scene.hierarchy.cycleRejected") {
|
|
SceneHost *h = SceneHost::createHost("cycle");
|
|
h->setTree(node("root", {node("a", {node("b")})}));
|
|
int root = h->findIndexById("root");
|
|
int a = h->findIndexById("a");
|
|
int b = h->findIndexById("b");
|
|
REQUIRE_GE(root, 0);
|
|
REQUIRE_GE(a, 0);
|
|
REQUIRE_GE(b, 0);
|
|
|
|
CHECK(!h->setParent(root, b));
|
|
CHECK(h->setParent(b, root));
|
|
CHECK_EQ(h->tree()->nodes[size_t(b)].parent, root);
|
|
}
|
|
|
|
TEST_CASE("Scene.SceneComponent.buildWhenAndSlot") {
|
|
BattleScene comp;
|
|
comp.slot = node("hud").withPosition(0.f, 1.f, 0.f);
|
|
comp.mountAs("battle_host");
|
|
|
|
SceneHost *h = Scene::create()->findHost("battle_host");
|
|
REQUIRE(h != nullptr);
|
|
CHECK(h->findById("ally") != nullptr);
|
|
CHECK(h->findById("hud") != nullptr);
|
|
|
|
comp.showAlly = false;
|
|
comp.markDirty();
|
|
CHECK(comp.updateIfDirty());
|
|
CHECK(h->findById("ally") == nullptr);
|
|
CHECK(h->findById("hud") != nullptr);
|
|
}
|
|
|
|
TEST_CASE("Scene.module.mountAndSetNode") {
|
|
Scene *mod = Scene::create();
|
|
mod->mountAs("main", node("root", {node("p").withPosition(0.f, 0.f, 0.f)}));
|
|
CHECK(mod->select("main"));
|
|
CHECK(mod->setNodePosition("p", 2.f, 3.f, 4.f));
|
|
TransformSystem::updateHost(mod->current());
|
|
SceneNode *p = mod->current()->findById("p");
|
|
REQUIRE(p != nullptr);
|
|
CHECK(approxEq(p->x, 2.f));
|
|
CHECK(approxEq(p->world[3][0], 2.f));
|
|
}
|
|
|
|
TEST_CASE("Scene.whenElse") {
|
|
NodeDesc a = whenElse(true, node("yes"), node("no"));
|
|
CHECK(a.id == "yes");
|
|
NodeDesc b = whenElse(false, node("yes"), node("no"));
|
|
CHECK(b.id == "no");
|
|
}
|
|
|
|
TEST_CASE("Scene.builder.mountBuildAs") {
|
|
Scene *mod = Scene::create();
|
|
mod->beginBuild();
|
|
mod->beginNode("root");
|
|
mod->setBuildPosition(1.f, 0.f, 0.f);
|
|
mod->addNode("child");
|
|
mod->end();
|
|
CHECK(mod->mountBuildAs("built"));
|
|
SceneHost *h = mod->findHost("built");
|
|
REQUIRE(h != nullptr);
|
|
CHECK(h->findById("root") != nullptr);
|
|
CHECK(h->findById("child") != nullptr);
|
|
CHECK(approxEq(h->findById("root")->x, 1.f));
|
|
}
|
|
|
|
TEST_CASE("Scene.link.syncRenderable3DWorld") {
|
|
SceneHost *h = SceneHost::createHost("link3d");
|
|
h->setTree(node("root", {node("mesh").withPosition(1.f, 2.f, 3.f)}).withPosition(10.f, 0.f, 0.f));
|
|
|
|
auto *r = eve::graphics::Renderable3D::create();
|
|
CHECK(h->linkRenderable3D("mesh", r));
|
|
TransformSystem::updateHost(h);
|
|
|
|
CHECK(approxEq(r->transform()->x, 11.f));
|
|
CHECK(approxEq(r->transform()->y, 2.f));
|
|
CHECK(approxEq(r->transform()->z, 3.f));
|
|
|
|
// Rebuild preserves link by id
|
|
h->setTree(node("root", {node("mesh").withPosition(0.f, 0.f, 0.f)}).withPosition(5.f, 0.f, 0.f));
|
|
TransformSystem::updateHost(h);
|
|
SceneNode *mesh = h->findById("mesh");
|
|
REQUIRE(mesh != nullptr);
|
|
CHECK(mesh->linkTarget == r);
|
|
CHECK(approxEq(r->transform()->x, 5.f));
|
|
}
|
|
|
|
TEST_CASE("Scene.link.syncRenderable2DWorld") {
|
|
SceneHost *h = SceneHost::createHost("link2d");
|
|
h->setTree(node("root", {node("spr").withSpace("2d").withPosition(2.f, 3.f, 0.f)})
|
|
.withSpace("2d")
|
|
.withPosition(4.f, 5.f, 0.f));
|
|
|
|
auto *r = eve::graphics::Renderable2D::create();
|
|
CHECK(h->linkRenderable2D("spr", r));
|
|
TransformSystem::updateHost(h);
|
|
CHECK(approxEq(r->transform()->x, 6.f));
|
|
CHECK(approxEq(r->transform()->y, 8.f));
|
|
}
|
|
|
|
TEST_CASE("Scene.query.pathParentChildren") {
|
|
SceneHost *h = SceneHost::createHost("query");
|
|
h->setTree(node("root",
|
|
{
|
|
node("player", {node("weapon").withName("gun")}),
|
|
node("enemy").withName("goblin"),
|
|
}));
|
|
|
|
CHECK(h->hasNode("weapon"));
|
|
CHECK_EQ(h->getNodeCount(), 4);
|
|
CHECK(h->getRoot()->id == "root");
|
|
CHECK(h->getParentById("weapon")->id == "player");
|
|
CHECK_EQ(h->getChildCountById("root"), 2);
|
|
CHECK(h->getChildAtById("root", 0)->id == "player");
|
|
CHECK(h->getPathById("weapon") == "root/player/weapon");
|
|
CHECK(h->findByPath("root/player/weapon")->id == "weapon");
|
|
CHECK(h->findByPath("player/weapon")->id == "weapon");
|
|
CHECK(h->findByName("gun")->id == "weapon");
|
|
CHECK(h->isAncestorOfById("root", "weapon"));
|
|
CHECK(h->isDescendantOfById("weapon", "player"));
|
|
CHECK(!h->isAncestorOfById("enemy", "weapon"));
|
|
}
|
|
|
|
TEST_CASE("Scene.query.walkFilterCollect") {
|
|
SceneHost *h = SceneHost::createHost("walk");
|
|
h->setTree(node("root",
|
|
{
|
|
node("a", {node("a1")}),
|
|
node("b").withVisible(false),
|
|
}));
|
|
|
|
std::vector<std::string> dfs;
|
|
h->walkDepthFirst([&](SceneHost *, int, SceneNode &n) { dfs.push_back(n.id); });
|
|
REQUIRE_EQ(dfs.size(), 4u);
|
|
CHECK(dfs[0] == "root");
|
|
CHECK(dfs[1] == "a");
|
|
CHECK(dfs[2] == "a1");
|
|
CHECK(dfs[3] == "b");
|
|
|
|
std::vector<std::string> bfs;
|
|
h->walkBreadthFirst([&](SceneHost *, int, SceneNode &n) { bfs.push_back(n.id); });
|
|
REQUIRE_EQ(bfs.size(), 4u);
|
|
CHECK(bfs[0] == "root");
|
|
CHECK(bfs[1] == "a");
|
|
CHECK(bfs[2] == "b");
|
|
CHECK(bfs[3] == "a1");
|
|
|
|
auto hidden = h->findAllVisible(false);
|
|
REQUIRE_EQ(hidden.size(), 1u);
|
|
CHECK(hidden[0]->id == "b");
|
|
|
|
auto underA = h->collectIdsFrom(h->findIndexById("a"));
|
|
REQUIRE_EQ(underA.size(), 2u);
|
|
CHECK(underA[0] == "a");
|
|
CHECK(underA[1] == "a1");
|
|
|
|
SceneNode *found = h->findIf([&](SceneHost *, int, const SceneNode &n) { return n.id == "a1"; });
|
|
REQUIRE(found != nullptr);
|
|
CHECK(found->id == "a1");
|
|
}
|
|
|
|
TEST_CASE("Scene.module.queryWrappers") {
|
|
Scene *mod = Scene::create();
|
|
mod->mountAs("q", node("root", {node("p", {node("c").withName("child")})}));
|
|
CHECK(mod->select("q"));
|
|
CHECK(mod->hasNode("c"));
|
|
CHECK(mod->getRootId() == "root");
|
|
CHECK(mod->getParentId("c") == "p");
|
|
CHECK_EQ(mod->getChildCount("root"), 1);
|
|
CHECK(mod->getChildIdAt("p", 0) == "c");
|
|
CHECK(mod->findIdByName("child") == "c");
|
|
CHECK(mod->findIdByPath("root/p/c") == "c");
|
|
CHECK(mod->getNodePath("c") == "root/p/c");
|
|
CHECK(mod->isAncestor("root", "c"));
|
|
CHECK(mod->isDescendant("c", "p"));
|
|
auto ids = mod->collectIds();
|
|
CHECK_EQ(ids.size(), 3u);
|
|
auto bfs = mod->walkBreadthFirstIds();
|
|
CHECK_EQ(bfs.size(), 3u);
|
|
CHECK(bfs[0] == "root");
|
|
CHECK(bfs[1] == "p");
|
|
CHECK(bfs[2] == "c");
|
|
}
|
|
|
|
TEST_CASE("Scene.render.parentChildOrbitPreview") {
|
|
if (ecs::current()->getManager<Renderable2D>() != nullptr) {
|
|
auto view = ecs::View<Renderable2D, Renderable2D::Sprite>();
|
|
for (auto it = view.begin(); it != view.end(); ++it) {
|
|
auto [sp] = *it;
|
|
sp->visible = false;
|
|
}
|
|
}
|
|
|
|
auto *win = eve::window::Window::create();
|
|
auto *gfx = Graphics::create();
|
|
REQUIRE(win != nullptr);
|
|
REQUIRE(gfx != nullptr);
|
|
win->setGraphics(gfx);
|
|
eve::window::WindowSettings s;
|
|
s.width = 640;
|
|
s.height = 420;
|
|
s.centered = true;
|
|
REQUIRE(win->setWindowSettings(s));
|
|
|
|
SceneHost *h = SceneHost::createHost("orbit");
|
|
h->setTree(node("root",
|
|
{
|
|
node("hub")
|
|
.withSpace("2d")
|
|
.withPosition(320.f, 210.f, 0.f)
|
|
.withRotation(0.f, 0.f, 0.f),
|
|
node("arm",
|
|
{
|
|
node("hand").withSpace("2d").withPosition(90.f, 0.f, 0.f),
|
|
})
|
|
.withSpace("2d")
|
|
.withPosition(320.f, 210.f, 0.f),
|
|
node("moon").withSpace("2d").withPosition(420.f, 210.f, 0.f),
|
|
})
|
|
.withSpace("2d"));
|
|
|
|
auto *hub = Renderable2D::create();
|
|
hub->sprite()->width = 48;
|
|
hub->sprite()->height = 48;
|
|
hub->sprite()->r = 0.95f;
|
|
hub->sprite()->g = 0.75f;
|
|
hub->sprite()->b = 0.25f;
|
|
hub->sprite()->visible = true;
|
|
|
|
auto *hand = Renderable2D::create();
|
|
hand->sprite()->width = 28;
|
|
hand->sprite()->height = 28;
|
|
hand->sprite()->r = 0.35f;
|
|
hand->sprite()->g = 0.85f;
|
|
hand->sprite()->b = 1.f;
|
|
hand->sprite()->visible = true;
|
|
|
|
auto *moon = Renderable2D::create();
|
|
moon->sprite()->width = 22;
|
|
moon->sprite()->height = 22;
|
|
moon->sprite()->r = 0.85f;
|
|
moon->sprite()->g = 0.45f;
|
|
moon->sprite()->b = 0.95f;
|
|
moon->sprite()->visible = true;
|
|
|
|
REQUIRE(h->linkRenderable2D("hub", hub));
|
|
REQUIRE(h->linkRenderable2D("hand", hand));
|
|
REQUIRE(h->linkRenderable2D("moon", moon));
|
|
|
|
auto *cam = Camera2D::createCamera();
|
|
cam->data()->r = 0.08f;
|
|
cam->data()->g = 0.09f;
|
|
cam->data()->b = 0.12f;
|
|
|
|
float handTravel = 0.f;
|
|
float prevHX = hand->transform()->x;
|
|
for (int frame = 0; frame < 90; ++frame) {
|
|
const float t = float(frame) * 0.06f;
|
|
SceneNode *arm = h->findById("arm");
|
|
SceneNode *moonN = h->findById("moon");
|
|
REQUIRE(arm != nullptr);
|
|
REQUIRE(moonN != nullptr);
|
|
arm->roll = t;
|
|
arm->localDirty = true;
|
|
moonN->x = 320.f + std::cos(t * 1.4f) * 120.f;
|
|
moonN->y = 210.f + std::sin(t * 1.4f) * 70.f;
|
|
moonN->localDirty = true;
|
|
|
|
TransformSystem::updateHost(h);
|
|
RenderSystem::render(*gfx);
|
|
|
|
handTravel += std::fabs(hand->transform()->x - prevHX);
|
|
prevHX = hand->transform()->x;
|
|
|
|
SDL_Event e;
|
|
while (SDL_PollEvent(&e)) {
|
|
if (e.type == SDL_QUIT) break;
|
|
}
|
|
SDL_Delay(16);
|
|
}
|
|
|
|
CHECK_GT(handTravel, 50.f);
|
|
hub->sprite()->visible = false;
|
|
hand->sprite()->visible = false;
|
|
moon->sprite()->visible = false;
|
|
win->close();
|
|
}
|