diff --git a/cmake/module_manifest.cmake b/cmake/module_manifest.cmake index a870bed4..1c235b50 100644 --- a/cmake/module_manifest.cmake +++ b/cmake/module_manifest.cmake @@ -142,6 +142,7 @@ eve_declare_module(NAME touch LAYER 2 SCRIPT Touch SLOT touch GROUP minimal 2d 3d web) eve_declare_module(NAME audio LAYER 2 SCRIPT Audio SLOT audio DEPS event sound + OPTIONAL_DEPS scene THIRDPARTY openal GROUP 2d 3d) eve_declare_module(NAME font LAYER 2 SCRIPT Font SLOT font @@ -176,6 +177,7 @@ eve_declare_module(NAME ui LIB EVUI LAYER 4 SCRIPT UI SLOT ui GROUP minimal 2d 3d web) eve_declare_module(NAME physics LAYER 4 SCRIPT Physics SLOT physics DEPS event graphics + OPTIONAL_DEPS scene THIRDPARTY box2d box3d GROUP 2d 3d web) eve_declare_module(NAME map LAYER 4 SCRIPT Map SLOT map @@ -214,9 +216,11 @@ eve_declare_module(NAME demo LAYER 4 SCRIPT Demo # L5 / L6 -- aggregates and orchestration # --------------------------------------------------------------------------- -eve_declare_module(NAME scene LAYER 5 SCRIPT Scene SLOT scene - DEPS graphics physics spatial - OPTIONAL_DEPS audio +# Renderables, bodies and audio sources attach through registered link kinds +# (scene/SceneLink.h), so scene no longer depends on those modules; graphics +# remains only because pickScreenAt / collectFrustumIdsAt take a Camera3D. +eve_declare_module(NAME scene LAYER 1 SCRIPT Scene SLOT scene + DEPS graphics spatial THIRDPARTY poco GROUP 3d web) eve_declare_module(NAME particles LAYER 5 SCRIPT Particles SLOT particles diff --git a/scripts/module_depgraph.py b/scripts/module_depgraph.py index 61df0d7d..fb06e1f8 100755 --- a/scripts/module_depgraph.py +++ b/scripts/module_depgraph.py @@ -33,6 +33,11 @@ KNOWN_BACK_EDGES = { # pointer and decodes its own icon image. ("window", "graphics"), ("window", "image"), + # scene/Scene.cpp: pickScreenAt and collectFrustumIdsAt take a + # graphics::Camera3D and read its projection. Everything else about scene is + # now renderer-agnostic (graphics registers its own link kinds), so these two + # entry points are what is left to move out to camera/graphics. + ("scene", "graphics"), } diff --git a/src/engine/devtools/SceneInspect.cpp b/src/engine/devtools/SceneInspect.cpp index 325d419a..17e7655b 100644 --- a/src/engine/devtools/SceneInspect.cpp +++ b/src/engine/devtools/SceneInspect.cpp @@ -122,7 +122,7 @@ image::ImageData* buildIdMask(eve::graphics::Graphics* gfx, std::string* outJson host->walkDepthFirst([&](eve::scene::SceneHost*, int, eve::scene::SceneNode& node) { if (!node.visible || node.space != "3d") return; - const auto* l = host->findLink(&node, eve::scene::LinkKind::Renderable3D); + const auto* l = host->findLink(&node, eve::scene::findLinkKind("renderable3d")); if (!l || !l->target) return; auto* r = static_cast(l->target); auto mr = r->meshRenderer(); diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt index 8a25278b..daecae00 100644 --- a/src/modules/CMakeLists.txt +++ b/src/modules/CMakeLists.txt @@ -13,6 +13,15 @@ if(NOT _has_font) CACHE INTERNAL "Exact basenames excluded from module source scan") endif() +# Each of graphics / physics / audio has a SceneLinks.cpp registering the link +# kinds it owns (scene/SceneLink.h). They all share the basename, so dropping +# scene drops all three in one rule. +eve_module_enabled(scene _has_scene) +if(NOT _has_scene) + set(EVENGINE_EXCLUDED_MODULE_FILES "${EVENGINE_EXCLUDED_MODULE_FILES};SceneLinks.cpp" + CACHE INTERNAL "Exact basenames excluded from module source scan") +endif() + # --- create every enabled module ------------------------------------------- foreach(_mod IN LISTS EVE_ENABLED_MODULES) @@ -167,7 +176,6 @@ if(_has_procgen) ${CMAKE_SOURCE_DIR}/third-party/DungeonTemplateLibrary/include) endif() -eve_module_enabled(scene _has_scene) if(_has_scene AND NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") # Scene serialization needs Poco JSON; the browser build has no Poco headers, # so EVENGINE_SCENE_JSON compiles that path out of Scene.cpp there. diff --git a/src/modules/audio/SceneLinks.cpp b/src/modules/audio/SceneLinks.cpp new file mode 100644 index 00000000..f3939445 --- /dev/null +++ b/src/modules/audio/SceneLinks.cpp @@ -0,0 +1,26 @@ +// Registers the scene link kind backed by audio: a positional 3D source that +// follows its node. +// +// audio::Source has no liveness API, so no `alive` callback is supplied and the +// link is assumed alive until explicitly unlinked. + +#include "audio/Source.h" +#include "scene/SceneHost.h" +#include "scene/SceneLink.h" + +namespace eve::audio { +namespace { + +using eve::scene::SceneNode; + +void pushSource3D(const SceneNode &n, void *target) { + auto *s = static_cast(target); + s->setPosition(n.world[3][0], n.world[3][1], n.world[3][2]); +} + +struct Register { + Register() { eve::scene::registerLinkKind({"audio3d", &pushSource3D, nullptr, nullptr}); } +} g_register; + +} // namespace +} // namespace eve::audio diff --git a/src/modules/graphics/SceneLinks.cpp b/src/modules/graphics/SceneLinks.cpp new file mode 100644 index 00000000..c2a03656 --- /dev/null +++ b/src/modules/graphics/SceneLinks.cpp @@ -0,0 +1,93 @@ +// Registers the scene link kinds backed by graphics: 2D and 3D renderables and +// the 3D camera. +// +// The world-matrix decomposition used to live in scene/TransformSystem.cpp, +// which forced scene to include graphics. Keeping it here puts the casts where +// the types are complete and lets scene stay unaware of what it is driving. + +#include "common/ECS.h" +#include "graphics/RenderSystem.h" +#include "graphics/RenderSystem3D.h" +#include "scene/SceneHost.h" +#include "scene/SceneLink.h" + +#include +#include +#include + +namespace eve::graphics { +namespace { + +using eve::scene::LinkOps; +using eve::scene::SceneNode; + +void pushRenderable3D(const SceneNode &n, void *target) { + auto *r = static_cast(target); + auto t = r->transform(); + const glm::mat4 &w = n.world; + t->x = w[3][0]; + t->y = w[3][1]; + t->z = w[3][2]; + + glm::vec3 c0(w[0]); + glm::vec3 c1(w[1]); + glm::vec3 c2(w[2]); + t->sx = glm::length(c0); + t->sy = glm::length(c1); + t->sz = glm::length(c2); + if (t->sx > 1e-8f) c0 /= t->sx; + if (t->sy > 1e-8f) c1 /= t->sy; + if (t->sz > 1e-8f) c2 /= t->sz; + + glm::mat3 rot(c0, c1, c2); + glm::quat q = glm::quat_cast(rot); + glm::vec3 euler = glm::eulerAngles(q); // pitch(x), yaw(y), roll(z) + t->pitch = euler.x; + t->yaw = euler.y; + t->roll = euler.z; + + r->meshRenderer()->visible = n.visible; +} + +void pushRenderable2D(const SceneNode &n, void *target) { + auto *r = static_cast(target); + auto t = r->transform(); + const glm::mat4 &w = n.world; + t->x = w[3][0]; + t->y = w[3][1]; + + glm::vec3 c0(w[0]); + glm::vec3 c1(w[1]); + t->sx = glm::length(c0); + t->sy = glm::length(c1); + if (t->sx > 1e-8f) c0 /= t->sx; + // 2D rotation from first column in XY + t->rot = std::atan2(c0.y, c0.x); + + r->sprite()->visible = n.visible; +} + +void pushCamera3D(const SceneNode &n, void *target) { + auto *c = static_cast(target); + c->setEye(n.world[3][0], n.world[3][1], n.world[3][2]); +} + +/** These are all ECS entities, destroyed through ecs::DestroyEntity. */ +template +bool entityAlive(const void *target) { + return ecs::is_entity_visible(static_cast(static_cast(target))); +} + +struct Register { + Register() { + eve::scene::registerLinkKind( + {"renderable2d", &pushRenderable2D, nullptr, &entityAlive}); + eve::scene::registerLinkKind( + {"renderable3d", &pushRenderable3D, nullptr, &entityAlive}); + eve::scene::registerLinkKind( + {"camera3d", &pushCamera3D, nullptr, &entityAlive}); + } +} g_register; + +} // namespace +} // namespace eve::graphics diff --git a/src/modules/physics/SceneLinks.cpp b/src/modules/physics/SceneLinks.cpp new file mode 100644 index 00000000..b215a435 --- /dev/null +++ b/src/modules/physics/SceneLinks.cpp @@ -0,0 +1,69 @@ +// Registers the scene link kinds backed by physics: 2D and 3D rigid bodies. +// +// These are the only kinds that can drive a node instead of following it +// (syncMode 1), so they are also the only ones supplying pullWorld. + +#include "physics/Body.h" +#include "physics/Body3D.h" +#include "scene/SceneHost.h" +#include "scene/SceneLink.h" + +#include + +namespace eve::physics { +namespace { + +using eve::scene::LinkOps; +using eve::scene::SceneNode; + +void pushBody2D(const SceneNode &n, void *target) { + auto *b = static_cast(target); + b->setPosition(n.x, n.y); + b->setAngle(n.roll); +} + +void pullBody2D(SceneNode &n, void *target) { + auto *b = static_cast(target); + n.x = b->getX(); + n.y = b->getY(); + n.roll = b->getAngle(); +} + +bool aliveBody2D(const void *target) { + return static_cast(target)->raw() != nullptr; +} + +void pushBody3D(const SceneNode &n, void *target) { + auto *b = static_cast(target); + b->setPosition(n.x, n.y, n.z); + glm::quat q = glm::angleAxis(n.yaw, glm::vec3(0.f, 1.f, 0.f)) * + glm::angleAxis(n.pitch, glm::vec3(1.f, 0.f, 0.f)) * + glm::angleAxis(n.roll, glm::vec3(0.f, 0.f, 1.f)); + b->setRotation(q.x, q.y, q.z, q.w); +} + +void pullBody3D(SceneNode &n, void *target) { + auto *b = static_cast(target); + n.x = b->getX(); + n.y = b->getY(); + n.z = b->getZ(); + glm::quat q(b->getRotW(), b->getRotX(), b->getRotY(), b->getRotZ()); + glm::vec3 e = glm::eulerAngles(q); // pitch(x), yaw(y), roll(z) + n.pitch = e.x; + n.yaw = e.y; + n.roll = e.z; +} + +bool aliveBody3D(const void *target) { + return static_cast(target)->isValid(); +} + +struct Register { + Register() { + eve::scene::registerLinkKind({"physics2d", &pushBody2D, &pullBody2D, &aliveBody2D}); + eve::scene::registerLinkKind({"physics3d", &pushBody3D, &pullBody3D, &aliveBody3D}); + } +} g_register; + +} // namespace +} // namespace eve::physics diff --git a/src/modules/scene/Scene.cpp b/src/modules/scene/Scene.cpp index 56cdf0ae..02f9ec5a 100644 --- a/src/modules/scene/Scene.cpp +++ b/src/modules/scene/Scene.cpp @@ -77,15 +77,12 @@ bool sameScriptObject(const HSQOBJECT &a, const HSQOBJECT &b) { std::memcmp(&a._unVal, &b._unVal, sizeof(a._unVal)) == 0; } -LinkKind linkKindFromString(const std::string &kind) { - if (kind == "renderable2d") return LinkKind::Renderable2D; - if (kind == "renderable3d") return LinkKind::Renderable3D; - if (kind == "physics2d") return LinkKind::Physics2D; - if (kind == "physics3d") return LinkKind::Physics3D; - if (kind == "camera3d") return LinkKind::Camera3D; - if (kind == "audio3d") return LinkKind::Audio3D; - return LinkKind::None; -} +/** + * The script API already names link kinds with strings, which is exactly the + * registry's key, so an unknown name and a kind whose module was trimmed out + * are the same case: -1. + */ +int linkKindFromString(const std::string &kind) { return findLinkKind(kind.c_str()); } int syncModeFromString(const std::string &mode) { return mode == "body" ? 1 : 0; @@ -898,8 +895,8 @@ bool Scene::unlinkNodeKindAt(const std::string &hostName, const std::string &nod const std::string &kind) { SceneHost *h = resolveHost(hostName); if (!h) return false; - const LinkKind k = linkKindFromString(kind); - if (k == LinkKind::None) return false; + const int k = linkKindFromString(kind); + if (k < 0) return false; const bool removed = h->unlink(nodeId, k); if (removed) TransformSystem::updateHost(h); return removed; diff --git a/src/modules/scene/SceneHost.cpp b/src/modules/scene/SceneHost.cpp index 5f674a25..00355458 100644 --- a/src/modules/scene/SceneHost.cpp +++ b/src/modules/scene/SceneHost.cpp @@ -53,7 +53,7 @@ void linkAsLastChild(SceneHost::Tree &tree, int parentIndex, int childIndex) { } /** Add a link of a kind, replacing an existing same-kind link (keep others). */ -bool setLink(SceneNode &n, LinkKind kind, void *target, int syncMode) { +bool setLink(SceneNode &n, int kind, void *target, int syncMode) { for (auto &l : n.links) { if (l.kind == kind) { l.target = target; @@ -522,37 +522,39 @@ std::vector SceneHost::collectIdsVisible(bool visible) { return collectIdsWhere([&](SceneHost *, int, const SceneNode &n) { return n.visible == visible; }); } -bool SceneHost::linkRenderable2D(const std::string &nodeId, graphics::Renderable2D *r) { +bool SceneHost::link(const std::string &nodeId, int kind, void *target, int syncMode) { + if (!linkOps(kind)) return false; // kind's module is not in this build SceneNode *n = findById(nodeId); - return n ? setLink(*n, LinkKind::Renderable2D, r, 0) : false; + return n ? setLink(*n, kind, target, syncMode) : false; +} + +// The typed helpers resolve their kind by name, so they return false rather +// than crashing when the module that registers it was trimmed out. +bool SceneHost::linkRenderable2D(const std::string &nodeId, graphics::Renderable2D *r) { + return link(nodeId, findLinkKind("renderable2d"), r, 0); } bool SceneHost::linkRenderable3D(const std::string &nodeId, graphics::Renderable3D *r) { - SceneNode *n = findById(nodeId); - return n ? setLink(*n, LinkKind::Renderable3D, r, 0) : false; + return link(nodeId, findLinkKind("renderable3d"), r, 0); } bool SceneHost::linkPhysics2D(const std::string &nodeId, physics::Body *b, int syncMode) { - SceneNode *n = findById(nodeId); - return n ? setLink(*n, LinkKind::Physics2D, b, syncMode) : false; + return link(nodeId, findLinkKind("physics2d"), b, syncMode); } bool SceneHost::linkPhysics3D(const std::string &nodeId, physics::Body3D *b, int syncMode) { - SceneNode *n = findById(nodeId); - return n ? setLink(*n, LinkKind::Physics3D, b, syncMode) : false; + return link(nodeId, findLinkKind("physics3d"), b, syncMode); } bool SceneHost::linkCamera3D(const std::string &nodeId, graphics::Camera3D *c) { - SceneNode *n = findById(nodeId); - return n ? setLink(*n, LinkKind::Camera3D, c, 0) : false; + return link(nodeId, findLinkKind("camera3d"), c, 0); } bool SceneHost::linkAudio3D(const std::string &nodeId, audio::Source *s) { - SceneNode *n = findById(nodeId); - return n ? setLink(*n, LinkKind::Audio3D, s, 0) : false; + return link(nodeId, findLinkKind("audio3d"), s, 0); } -bool SceneHost::unlink(const std::string &nodeId, LinkKind kind) { +bool SceneHost::unlink(const std::string &nodeId, int kind) { SceneNode *n = findById(nodeId); if (!n) return false; for (auto it = n->links.begin(); it != n->links.end(); ++it) { @@ -571,7 +573,7 @@ bool SceneHost::unlink(const std::string &nodeId) { return true; } -SceneLink *SceneHost::findLink(SceneNode *node, LinkKind kind) { +SceneLink *SceneHost::findLink(SceneNode *node, int kind) { if (!node) return nullptr; for (auto &l : node->links) { if (l.kind == kind) return &l; @@ -579,7 +581,7 @@ SceneLink *SceneHost::findLink(SceneNode *node, LinkKind kind) { return nullptr; } -const SceneLink *SceneHost::findLink(const SceneNode *node, LinkKind kind) const { +const SceneLink *SceneHost::findLink(const SceneNode *node, int kind) const { if (!node) return nullptr; for (const auto &l : node->links) { if (l.kind == kind) return &l; diff --git a/src/modules/scene/SceneHost.h b/src/modules/scene/SceneHost.h index 1f308abb..1be44361 100644 --- a/src/modules/scene/SceneHost.h +++ b/src/modules/scene/SceneHost.h @@ -12,7 +12,10 @@ #include #include "scene/NodeDesc.h" +#include "scene/SceneLink.h" +// Only pointers to these cross the interface, so a forward declaration is +// enough: scene links carry them as void* and the owning module does the casts. namespace eve::graphics { class Renderable2D; class Renderable3D; @@ -30,29 +33,6 @@ class Source; namespace eve::scene { -/** Link target kind for the generic per-node link system. */ -enum class LinkKind { - None = 0, - Renderable2D, - Renderable3D, - Physics2D, - Physics3D, - Camera3D, - Audio3D, -}; - -/** - * One generic link between a scene node and an external object (renderable, - * physics body, camera, audio source). A node can hold several links of - * different kinds; re-linking the same kind replaces the target. - * syncMode: 0 = node → target (node authoritative), 1 = target → node. - */ -struct SceneLink { - LinkKind kind = LinkKind::None; - void *target = nullptr; - int syncMode = 0; -}; - /** * Retained scene node (arena). Conceptual GameObject; isomorphic to eve::ui::UINode. */ @@ -241,9 +221,15 @@ public: std::vector collectIdsVisible(bool visible = true); /** - * Link an external object to a node id. Survives reconcile/rebuild by id. - * After TransformSystem, the link is synced per kind (and syncMode). + * Attach an external object to a node id. Survives reconcile/rebuild by id. + * After TransformSystem the link is synced per kind (and syncMode). + * + * `kind` is an id from scene::registerLinkKind, so any module can attach to + * the tree; the typed helpers below are conveniences for the kinds the + * engine ships. Returns false when the node or the kind is unknown. */ + bool link(const std::string &nodeId, int kind, void *target, int syncMode = 0); + bool linkRenderable2D(const std::string &nodeId, graphics::Renderable2D *r); bool linkRenderable3D(const std::string &nodeId, graphics::Renderable3D *r); bool linkPhysics2D(const std::string &nodeId, physics::Body *b, int syncMode = 0); @@ -252,12 +238,12 @@ public: bool linkAudio3D(const std::string &nodeId, audio::Source *s); /** Remove all links of a kind; returns true if a link was removed. */ - bool unlink(const std::string &nodeId, LinkKind kind); + bool unlink(const std::string &nodeId, int kind); /** Remove every link on the node. */ bool unlink(const std::string &nodeId); - SceneLink *findLink(SceneNode *node, LinkKind kind); - const SceneLink *findLink(const SceneNode *node, LinkKind kind) const; + SceneLink *findLink(SceneNode *node, int kind); + const SceneLink *findLink(const SceneNode *node, int kind) const; int linkCount(const std::string &nodeId); /** Reparent by id; empty parentId detaches (parent = -1). Cycle-safe. */ diff --git a/src/modules/scene/SceneLink.cpp b/src/modules/scene/SceneLink.cpp new file mode 100644 index 00000000..0e8ef45d --- /dev/null +++ b/src/modules/scene/SceneLink.cpp @@ -0,0 +1,58 @@ +#include "scene/SceneLink.h" + +#include +#include +#include + +namespace eve::scene { +namespace { + +struct Entry { + std::string name; + LinkOps ops; +}; + +/** + * Function-local so registration from another module's static initialiser + * cannot run before the table exists. Ids are indices and never reused, so a + * SceneLink's kind stays valid for the life of the process. + */ +std::vector& table() { + static std::vector t; + return t; +} + +} // namespace + +int registerLinkKind(const LinkOps& ops) { + if (!ops.kind || !ops.pushWorld) return -1; + const int existing = findLinkKind(ops.kind); + if (existing >= 0) return existing; + table().push_back({ops.kind, ops}); + // Point the stored ops at our own copy of the name so a caller may pass a + // temporary. + Entry& e = table().back(); + e.ops.kind = e.name.c_str(); + return int(table().size()) - 1; +} + +int findLinkKind(const char* kind) { + if (!kind) return -1; + const auto& t = table(); + for (size_t i = 0; i < t.size(); ++i) + if (t[i].name == kind) return int(i); + return -1; +} + +const LinkOps* linkOps(int kindId) { + const auto& t = table(); + if (kindId < 0 || size_t(kindId) >= t.size()) return nullptr; + return &t[size_t(kindId)].ops; +} + +const char* linkKindName(int kindId) { + const LinkOps* ops = linkOps(kindId); + return ops ? ops->kind : ""; +} + +} // namespace eve::scene diff --git a/src/modules/scene/SceneLink.h b/src/modules/scene/SceneLink.h new file mode 100644 index 00000000..cdae3b31 --- /dev/null +++ b/src/modules/scene/SceneLink.h @@ -0,0 +1,74 @@ +#pragma once + +// Attaching things to scene nodes. +// +// A link keeps an external object -- a renderable, a physics body, a camera, an +// audio source -- in step with the node it hangs off. The set of kinds used to +// be a closed enum with a switch in TransformSystem, so scene had to include +// graphics, physics and audio, and no other module could ever attach to the +// tree without editing this file. +// +// Kinds are registered instead. The module that owns the target type supplies +// the three operations, keeps the casts on its own side where the type is +// complete, and scene never learns what a Renderable3D is. +// +// Dispatch is through a function-pointer table, not virtual calls: link sync +// runs for every dirty node every frame. + +#include "common/Export.h" + +#include + +namespace eve::scene { + +struct SceneNode; + +/** Behaviour for one kind of link target, supplied by the owning module. */ +struct LinkOps { + /** Stable name, e.g. "renderable3d". Registration is idempotent by name. */ + const char* kind = nullptr; + + /** Node -> target, after the node's world matrix is recomputed. */ + void (*pushWorld)(const SceneNode& node, void* target) = nullptr; + + /** + * Target -> node, before world propagation, for links whose target is + * authoritative (syncMode 1). Null when the kind cannot drive a node. + */ + void (*pullWorld)(SceneNode& node, void* target) = nullptr; + + /** + * Whether the target is still alive. Dead links are dropped rather than + * followed into freed memory. Null means always alive. + */ + bool (*alive)(const void* target) = nullptr; +}; + +/** + * Register a link kind and return its id. Registering the same name twice + * returns the first id and leaves the original operations in place, so a + * module reloaded as a plugin cannot silently swap them out. + */ +EVENGINE_API int registerLinkKind(const LinkOps& ops); + +/** Id for a previously registered kind, or -1. */ +EVENGINE_API int findLinkKind(const char* kind); + +/** Operations for an id, or nullptr when the id is unknown. */ +EVENGINE_API const LinkOps* linkOps(int kindId); + +/** Name for an id, or "" when the id is unknown. */ +EVENGINE_API const char* linkKindName(int kindId); + +/** + * One link between a scene node and an external object. The node may hold + * several links of different kinds; re-linking the same kind replaces the + * target. syncMode: 0 = node drives target, 1 = target drives node. + */ +struct SceneLink { + int kind = -1; + void* target = nullptr; + int syncMode = 0; +}; + +} // namespace eve::scene diff --git a/src/modules/scene/TransformSystem.cpp b/src/modules/scene/TransformSystem.cpp index dd88170d..69cdb6d4 100644 --- a/src/modules/scene/TransformSystem.cpp +++ b/src/modules/scene/TransformSystem.cpp @@ -1,14 +1,7 @@ #include "scene/TransformSystem.h" #include "scene/SceneHost.h" - -#ifdef EVENGINE_SCENE_AUDIO -#include "audio/Source.h" -#endif -#include "graphics/RenderSystem.h" -#include "graphics/RenderSystem3D.h" -#include "physics/Body.h" -#include "physics/Body3D.h" +#include "scene/SceneLink.h" #include #include @@ -40,50 +33,6 @@ void updateNode(SceneHost::Tree &tree, int nodeIndex, const glm::mat4 &parentWor } } -void writeWorldToRenderable3D(const SceneNode &n, graphics::Renderable3D *r) { - auto t = r->transform(); - const glm::mat4 &w = n.world; - t->x = w[3][0]; - t->y = w[3][1]; - t->z = w[3][2]; - - glm::vec3 c0(w[0]); - glm::vec3 c1(w[1]); - glm::vec3 c2(w[2]); - t->sx = glm::length(c0); - t->sy = glm::length(c1); - t->sz = glm::length(c2); - if (t->sx > 1e-8f) c0 /= t->sx; - if (t->sy > 1e-8f) c1 /= t->sy; - if (t->sz > 1e-8f) c2 /= t->sz; - - glm::mat3 rot(c0, c1, c2); - glm::quat q = glm::quat_cast(rot); - glm::vec3 euler = glm::eulerAngles(q); // pitch(x), yaw(y), roll(z) - t->pitch = euler.x; - t->yaw = euler.y; - t->roll = euler.z; - - r->meshRenderer()->visible = n.visible; -} - -void writeWorldToRenderable2D(const SceneNode &n, graphics::Renderable2D *r) { - auto t = r->transform(); - const glm::mat4 &w = n.world; - t->x = w[3][0]; - t->y = w[3][1]; - - glm::vec3 c0(w[0]); - glm::vec3 c1(w[1]); - t->sx = glm::length(c0); - t->sy = glm::length(c1); - if (t->sx > 1e-8f) c0 /= t->sx; - // 2D rotation from first column in XY - t->rot = std::atan2(c0.y, c0.x); - - r->sprite()->visible = n.visible; -} - /** syncMode == 1: pull target state into the node before world propagation. */ void pullTargets(SceneHost *host) { if (!host) return; @@ -92,29 +41,9 @@ void pullTargets(SceneHost *host) { SceneNode &n = t->nodes[i]; for (auto &l : n.links) { if (l.syncMode != 1 || !l.target) continue; - switch (l.kind) { - case LinkKind::Physics2D: { - auto *b = static_cast(l.target); - n.x = b->getX(); - n.y = b->getY(); - n.roll = b->getAngle(); - break; - } - case LinkKind::Physics3D: { - auto *b = static_cast(l.target); - n.x = b->getX(); - n.y = b->getY(); - n.z = b->getZ(); - glm::quat q(b->getRotW(), b->getRotX(), b->getRotY(), b->getRotZ()); - glm::vec3 e = glm::eulerAngles(q); // pitch(x), yaw(y), roll(z) - n.pitch = e.x; - n.yaw = e.y; - n.roll = e.z; - break; - } - default: - break; - } + const LinkOps *ops = linkOps(l.kind); + if (!ops || !ops->pullWorld) continue; + ops->pullWorld(n, l.target); host->markSubtreeDirty(int(i)); } } @@ -142,43 +71,8 @@ void updateNodeIncremental(SceneHost::Tree &tree, int nodeIndex, /** syncMode == 0: push node state into the linked target. */ void pushTarget(const SceneNode &n, const SceneLink &l) { if (!l.target) return; - switch (l.kind) { - case LinkKind::Renderable2D: - writeWorldToRenderable2D(n, static_cast(l.target)); - break; - case LinkKind::Renderable3D: - writeWorldToRenderable3D(n, static_cast(l.target)); - break; - case LinkKind::Physics2D: { - auto *b = static_cast(l.target); - b->setPosition(n.x, n.y); - b->setAngle(n.roll); - break; - } - case LinkKind::Physics3D: { - auto *b = static_cast(l.target); - b->setPosition(n.x, n.y, n.z); - glm::quat q = glm::angleAxis(n.yaw, glm::vec3(0.f, 1.f, 0.f)) * - glm::angleAxis(n.pitch, glm::vec3(1.f, 0.f, 0.f)) * - glm::angleAxis(n.roll, glm::vec3(0.f, 0.f, 1.f)); - b->setRotation(q.x, q.y, q.z, q.w); - break; - } - case LinkKind::Camera3D: { - auto *c = static_cast(l.target); - c->setEye(n.world[3][0], n.world[3][1], n.world[3][2]); - break; - } - case LinkKind::Audio3D: { -#ifdef EVENGINE_SCENE_AUDIO - auto *s = static_cast(l.target); - s->setPosition(n.world[3][0], n.world[3][1], n.world[3][2]); -#endif - break; - } - default: - break; - } + const LinkOps *ops = linkOps(l.kind); + if (ops) ops->pushWorld(n, l.target); } void syncLinks(SceneHost *host) { @@ -192,25 +86,15 @@ void syncLinks(SceneHost *host) { } /** - * Best-effort liveness check for a linked target. ECS entities (renderables, - * cameras) die via ecs::DestroyEntity; physics bodies expose raw()/isValid(). - * audio::Source has no liveness API, so it is always assumed alive. + * Best-effort liveness check for a linked target. Each kind knows how to answer + * for its own type; a kind that cannot tell (an audio source has no liveness + * API) leaves `alive` null and is assumed alive. */ bool linkTargetAlive(const SceneLink &l) { if (!l.target) return false; - switch (l.kind) { - case LinkKind::Renderable2D: - case LinkKind::Renderable3D: - case LinkKind::Camera3D: - return ecs::is_entity_visible(static_cast(l.target)); - case LinkKind::Physics2D: - return static_cast(l.target)->raw() != nullptr; - case LinkKind::Physics3D: - return static_cast(l.target)->isValid(); - case LinkKind::Audio3D: - default: - return true; - } + const LinkOps *ops = linkOps(l.kind); + if (!ops || !ops->alive) return true; + return ops->alive(l.target); } /** Drop links whose target was destroyed (prevents dangling-pointer sync). */ diff --git a/src/modules/sceneloader/SceneLoader.cpp b/src/modules/sceneloader/SceneLoader.cpp index 6b81224d..29db8302 100644 --- a/src/modules/sceneloader/SceneLoader.cpp +++ b/src/modules/sceneloader/SceneLoader.cpp @@ -652,7 +652,7 @@ bool SceneLoader::applyTreeDiff(scene::SceneHost *host, const scene::NodeDesc &n // Destroy Renderable3D of removed GameObjects. for (const auto &kv : oldNodes) { if (!newIds.count(kv.first)) { - if (const auto *l = host->findLink(kv.second, scene::LinkKind::Renderable3D)) { + if (const auto *l = host->findLink(kv.second, scene::findLinkKind("renderable3d"))) { destroyRenderable(static_cast(l->target)); } } @@ -728,7 +728,7 @@ bool SceneLoader::applyTreeDiff(scene::SceneHost *host, const scene::NodeDesc &n r = nullptr; } if (r) { - n.links.push_back(scene::SceneLink{scene::LinkKind::Renderable3D, r, 0}); + n.links.push_back(scene::SceneLink{scene::findLinkKind("renderable3d"), r, 0}); } } } @@ -763,7 +763,7 @@ void SceneLoader::linkMeshNodes(scene::SceneHost *host, const MeshSlotMap &slots for (const auto &kv : slots) { scene::SceneNode *n = host->findById(kv.first); if (!n || kv.second.empty()) continue; - if (host->findLink(n, scene::LinkKind::Renderable3D)) continue; + if (host->findLink(n, scene::findLinkKind("renderable3d"))) continue; const MeshSlot &slot = kv.second[0]; graphics::Mesh *mesh = nullptr; auto it = shared.find(slot.mesh); @@ -810,7 +810,7 @@ void SceneLoader::linkMeshNodes(scene::SceneHost *host, const MeshSlotMap &slots if (albedo) r->setTexture(albedo); if (normal) r->setNormalTexture(normal); if (height) r->setHeightTexture(height); - n->links.push_back(scene::SceneLink{scene::LinkKind::Renderable3D, r, 0}); + n->links.push_back(scene::SceneLink{scene::findLinkKind("renderable3d"), r, 0}); } } @@ -963,7 +963,7 @@ void SceneLoader::unload(const std::string &path) { if (it->second.host) { auto t = it->second.host->tree(); for (auto &n : t->nodes) { - if (const auto *l = it->second.host->findLink(&n, scene::LinkKind::Renderable3D)) { + if (const auto *l = it->second.host->findLink(&n, scene::findLinkKind("renderable3d"))) { destroyRenderable(static_cast(l->target)); } n.links.clear(); diff --git a/test/scene.cpp b/test/scene.cpp index 2a1a1972..dea89626 100644 --- a/test/scene.cpp +++ b/test/scene.cpp @@ -475,9 +475,9 @@ TEST_CASE("Scene.link.genericMultiLinkAndSync") { CHECK(approxEq(n->z, 30.f)); // unlink by kind keeps the other links - CHECK(h->unlink("soldier", LinkKind::Physics3D)); + CHECK(h->unlink("soldier", findLinkKind("physics3d"))); CHECK_EQ(h->linkCount("soldier"), 2); - CHECK(!h->unlink("soldier", LinkKind::Physics3D)); + CHECK(!h->unlink("soldier", findLinkKind("physics3d"))); // full rebuild preserves remaining links by id h->setTree(node("root", {node("soldier")})); @@ -519,7 +519,7 @@ TEST_CASE("Scene.link.physics2D") { CHECK(approxEq(n->y, 40.f)); CHECK(approxEq(n->roll, 0.5f)); - CHECK(h->unlink("p", LinkKind::Physics2D)); + CHECK(h->unlink("p", findLinkKind("physics2d"))); CHECK_EQ(h->linkCount("p"), 0); w.destroy(); } diff --git a/test/sceneloader.cpp b/test/sceneloader.cpp index a98cdf1b..d846a9c7 100644 --- a/test/sceneloader.cpp +++ b/test/sceneloader.cpp @@ -204,7 +204,7 @@ TEST_CASE("SceneLoader.apply.preservesUnchangedRenderableIdentity") { // The unchanged GameObject keeps its linked Renderable3D — no rebuild / re-upload. REQUIRE_EQ(after->links.size(), 1u); CHECK(after->links[0].target == r); - CHECK(int(after->links[0].kind) == int(eve::scene::LinkKind::Renderable3D)); + CHECK(after->links[0].kind == eve::scene::findLinkKind("renderable3d")); CHECK(h->findById("keep2") != nullptr); CHECK(h->findById("gone") == nullptr); @@ -258,7 +258,7 @@ TEST_CASE("SceneLoader.load.buildsGameObjectTreeWithRenderables") { std::vector linked = h->findAllLinked(); REQUIRE_EQ(linked.size(), 1u); REQUIRE_EQ(linked[0]->links.size(), 1u); - CHECK(int(linked[0]->links[0].kind) == int(eve::scene::LinkKind::Renderable3D)); + CHECK(linked[0]->links[0].kind == eve::scene::findLinkKind("renderable3d")); auto *r = static_cast(linked[0]->links[0].target); REQUIRE(r != nullptr); CHECK(r->meshRenderer()->mesh != nullptr);