ADD: glTF instance rotation, animation
This commit is contained in:
@@ -81,7 +81,33 @@ Docs: `docs/Scene.md`
|
|||||||
- `bool setGLTFInstanceAnimationLoop(const std::string &instanceName, bool loop);`
|
- `bool setGLTFInstanceAnimationLoop(const std::string &instanceName, bool loop);`
|
||||||
- Notes:
|
- Notes:
|
||||||
- All functions return `bool` indicating whether the named scene/instance exists.
|
- All functions return `bool` indicating whether the named scene/instance exists.
|
||||||
- `SceneManager::update_scene()` advances active animations each frame using engine delta time.
|
- Animation state is **independent per scene and per instance**:
|
||||||
|
- Each named scene has its own `AnimationState`.
|
||||||
|
- Each glTF instance has its own `AnimationState`, even when sharing the same `LoadedGLTF`.
|
||||||
|
- An index `< 0` (e.g. `-1`) disables animation for that scene/instance (pose is frozen at the last evaluated state).
|
||||||
|
- `SceneManager::update_scene()` advances each active animation state every frame using engine delta time.
|
||||||
|
|
||||||
|
### Per‑Instance Node / Joint Control (Non‑Skinned)
|
||||||
|
|
||||||
|
For rigid models and simple “joints” (e.g. flaps, doors, turrets), you can apply local‑space pose offsets to individual glTF nodes per instance:
|
||||||
|
|
||||||
|
- `bool setGLTFInstanceNodeOffset(const std::string &instanceName, const std::string &nodeName, const glm::mat4 &offset);`
|
||||||
|
- `bool clearGLTFInstanceNodeOffset(const std::string &instanceName, const std::string &nodeName);`
|
||||||
|
- `void clearGLTFInstanceNodeOffsets(const std::string &instanceName);`
|
||||||
|
|
||||||
|
Typical usage:
|
||||||
|
|
||||||
|
- Use glTF animation for the base motion (e.g. gear deployment).
|
||||||
|
- Layer game‑driven offsets on top for per‑instance control:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Rotate a control surface on one aircraft instance
|
||||||
|
glm::mat4 offset =
|
||||||
|
glm::rotate(glm::mat4(1.f),
|
||||||
|
glm::radians(aileronDegrees),
|
||||||
|
glm::vec3(1.f, 0.f, 0.f));
|
||||||
|
sceneMgr->setGLTFInstanceNodeOffset("plane01", "LeftAileron", offset);
|
||||||
|
```
|
||||||
|
|
||||||
### Point Lights
|
### Point Lights
|
||||||
|
|
||||||
@@ -163,4 +189,3 @@ These are primarily debug/editor features but can be kept in a game build to pro
|
|||||||
- Point‑light editor UI built on `SceneManager` light APIs.
|
- Point‑light editor UI built on `SceneManager` light APIs.
|
||||||
- Object gizmo (ImGuizmo):
|
- Object gizmo (ImGuizmo):
|
||||||
- Uses last pick / hover pick as the current target and manipulates transforms via `setMeshInstanceTransform` / `setGLTFInstanceTransform`.
|
- Uses last pick / hover pick as the current target and manipulates transforms via `setMeshInstanceTransform` / `setGLTFInstanceTransform`.
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,12 @@ Thin scene layer that produces `RenderObject`s for the renderer. It gathers opaq
|
|||||||
|
|
||||||
### GLTF Animation / “Actions”
|
### GLTF Animation / “Actions”
|
||||||
|
|
||||||
GLTF files can contain one or more animation clips (e.g. `Idle`, `Walk`, `Run`). The loader (`LoadedGLTF`) parses these into `LoadedGLTF::Animation` objects, and `SceneManager` exposes a thin API to pick which clip is currently playing.
|
GLTF files can contain one or more animation clips (e.g. `Idle`, `Walk`, `Run`). The loader (`LoadedGLTF`) parses these into `LoadedGLTF::Animation` objects. Animation *state* (which clip, time, loop flag) is stored outside the glTF asset:
|
||||||
|
|
||||||
> Note: a `LoadedGLTF` is typically shared by multiple instances. Changing the active animation on a shared `LoadedGLTF` will affect all instances that point to it. If you want per‑character independent actions, load separate `LoadedGLTF` objects (one per character) or duplicate the asset in your game layer.
|
- One `AnimationState` per named static scene (for `loadScene`).
|
||||||
|
- One `AnimationState` per runtime glTF instance (`SceneManager::GLTFInstance`).
|
||||||
|
|
||||||
|
This means that **animation is independent per scene and per instance**, even if they share the same underlying `LoadedGLTF` asset and meshes.
|
||||||
|
|
||||||
**Static scenes (loaded via `loadScene`)**
|
**Static scenes (loaded via `loadScene`)**
|
||||||
|
|
||||||
@@ -54,19 +57,19 @@ Example: engine default scene in `VulkanEngine::init()`:
|
|||||||
- `structure` is loaded and registered via:
|
- `structure` is loaded and registered via:
|
||||||
- `sceneManager->loadScene("structure", structureFile);`
|
- `sceneManager->loadScene("structure", structureFile);`
|
||||||
|
|
||||||
To control its animation:
|
To control its animation state:
|
||||||
|
|
||||||
- By index:
|
- By index (per‑scene state):
|
||||||
- `scene->setSceneAnimation("structure", 0); // first clip`
|
- `scene->setSceneAnimation("structure", 0); // first clip`
|
||||||
- `scene->setSceneAnimation("structure", 1, true); // second clip, reset time`
|
- `scene->setSceneAnimation("structure", 1, true); // second clip, reset time`
|
||||||
- By name (matches glTF animation name):
|
- By name (per‑scene state; matches glTF animation name):
|
||||||
- `scene->setSceneAnimation("structure", "Idle");`
|
- `scene->setSceneAnimation("structure", "Idle");`
|
||||||
- `scene->setSceneAnimation("structure", "Run");`
|
- `scene->setSceneAnimation("structure", "Run");`
|
||||||
- Looping:
|
- Looping (per‑scene state):
|
||||||
- `scene->setSceneAnimationLoop("structure", true); // enable loop`
|
- `scene->setSceneAnimationLoop("structure", true); // enable loop`
|
||||||
- `scene->setSceneAnimationLoop("structure", false); // play once and stop at end`
|
- `scene->setSceneAnimationLoop("structure", false); // play once and stop at end`
|
||||||
|
|
||||||
All functions return `bool` to indicate whether the scene name was found.
|
All functions return `bool` to indicate whether the scene name was found. A negative index (e.g. `-1`) disables animation for that scene (pose stays at the last evaluated frame).
|
||||||
|
|
||||||
**Runtime GLTF instances**
|
**Runtime GLTF instances**
|
||||||
|
|
||||||
@@ -74,17 +77,35 @@ GLTF instances are created via:
|
|||||||
|
|
||||||
- `scene->addGLTFInstance("player", playerGltf, playerTransform);`
|
- `scene->addGLTFInstance("player", playerGltf, playerTransform);`
|
||||||
|
|
||||||
You can treat each instance as an “actor” and drive its current action from your game state:
|
You can treat each instance as an “actor” and drive its current action from your game state. Each instance has its own `AnimationState`, even if multiple instances share the same `LoadedGLTF`.
|
||||||
|
|
||||||
- By index:
|
- By index (per‑instance state):
|
||||||
- `scene->setGLTFInstanceAnimation("player", 0);`
|
- `scene->setGLTFInstanceAnimation("player", 0);`
|
||||||
- By name:
|
- `scene->setGLTFInstanceAnimation("player", -1); // disable animation for this actor`
|
||||||
|
- By name (per‑instance state):
|
||||||
- `scene->setGLTFInstanceAnimation("player", "Idle");`
|
- `scene->setGLTFInstanceAnimation("player", "Idle");`
|
||||||
- `scene->setGLTFInstanceAnimation("player", "Run");`
|
- `scene->setGLTFInstanceAnimation("player", "Run");`
|
||||||
- Looping:
|
- Looping (per‑instance state):
|
||||||
- `scene->setGLTFInstanceAnimationLoop("player", true);`
|
- `scene->setGLTFInstanceAnimationLoop("player", true);`
|
||||||
|
|
||||||
These helpers forward to the underlying `LoadedGLTF`’s `setActiveAnimation(...)` and `animationLoop` fields. `SceneManager::update_scene()` advances animations every frame using a per‑frame `dt`, so once you select an action, it will keep playing automatically until you change it or disable looping.
|
These helpers update the instance’s `AnimationState`. `SceneManager::update_scene()` advances each instance’s state every frame using a per‑frame `dt` before drawing, so once you select an action, it will keep playing automatically until you change it or disable looping for that instance.
|
||||||
|
|
||||||
|
### Per‑Instance Node / Joint Overrides
|
||||||
|
|
||||||
|
For non‑skinned models (rigid parts), you can apply local‑space pose offsets to specific glTF nodes on a **per‑instance** basis. This is useful for things like control surfaces, doors, or turrets layered on top of an existing animation.
|
||||||
|
|
||||||
|
- API (on `SceneManager`):
|
||||||
|
- `bool setGLTFInstanceNodeOffset(const std::string &instanceName, const std::string &nodeName, const glm::mat4 &offset);`
|
||||||
|
- `bool clearGLTFInstanceNodeOffset(const std::string &instanceName, const std::string &nodeName);`
|
||||||
|
- `void clearGLTFInstanceNodeOffsets(const std::string &instanceName);`
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Offsets are **local‑space** post‑multipliers:
|
||||||
|
- Effective local transform = `node.localTransform * offset`.
|
||||||
|
- Offsets are *per instance*:
|
||||||
|
- Different instances of the same glTF can have different joint poses at the same animation time.
|
||||||
|
- Overrides are applied during draw via `DrawContext::gltfNodeLocalOverrides` and `MeshNode::Draw`, without modifying the shared glTF asset.
|
||||||
|
|
||||||
### GPU Scene Data
|
### GPU Scene Data
|
||||||
|
|
||||||
|
|||||||
@@ -1044,9 +1044,56 @@ void VulkanEngine::init_pipelines()
|
|||||||
metalRoughMaterial.build_pipelines(this);
|
metalRoughMaterial.build_pipelines(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// Rebuild a node's world transform in glTF local space, layering per-instance
|
||||||
|
// local offsets on top of the base localTransform at each node in the chain.
|
||||||
|
glm::mat4 build_node_world_with_overrides(const Node *node,
|
||||||
|
const std::unordered_map<const Node*, glm::mat4> &overrides)
|
||||||
|
{
|
||||||
|
if (!node)
|
||||||
|
{
|
||||||
|
return glm::mat4(1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<const Node*> chain;
|
||||||
|
const Node *cur = node;
|
||||||
|
while (cur)
|
||||||
|
{
|
||||||
|
chain.push_back(cur);
|
||||||
|
std::shared_ptr<Node> parent = cur->parent.lock();
|
||||||
|
cur = parent ? parent.get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::mat4 world(1.0f);
|
||||||
|
for (auto it = chain.rbegin(); it != chain.rend(); ++it)
|
||||||
|
{
|
||||||
|
const Node *n = *it;
|
||||||
|
glm::mat4 local = n->localTransform;
|
||||||
|
auto ovIt = overrides.find(n);
|
||||||
|
if (ovIt != overrides.end())
|
||||||
|
{
|
||||||
|
// Layer the override in local space for this instance.
|
||||||
|
local = local * ovIt->second;
|
||||||
|
}
|
||||||
|
world = world * local;
|
||||||
|
}
|
||||||
|
return world;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MeshNode::Draw(const glm::mat4 &topMatrix, DrawContext &ctx)
|
void MeshNode::Draw(const glm::mat4 &topMatrix, DrawContext &ctx)
|
||||||
{
|
{
|
||||||
glm::mat4 nodeMatrix = topMatrix * worldTransform;
|
glm::mat4 nodeMatrix;
|
||||||
|
if (ctx.gltfNodeLocalOverrides && !ctx.gltfNodeLocalOverrides->empty())
|
||||||
|
{
|
||||||
|
glm::mat4 world = build_node_world_with_overrides(this, *ctx.gltfNodeLocalOverrides);
|
||||||
|
nodeMatrix = topMatrix * world;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nodeMatrix = topMatrix * worldTransform;
|
||||||
|
}
|
||||||
|
|
||||||
if (!mesh)
|
if (!mesh)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -832,12 +832,8 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file.animations.empty())
|
// Default animation state is now owned by SceneManager per static scene / instance.
|
||||||
{
|
// LoadedGLTF only stores shared animation clips.
|
||||||
file.activeAnimation = 0;
|
|
||||||
file.animationTime = 0.0f;
|
|
||||||
file.animationLoop = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We no longer need glTF-owned buffer payloads; free any large vectors
|
// We no longer need glTF-owned buffer payloads; free any large vectors
|
||||||
@@ -896,62 +892,72 @@ void LoadedGLTF::refreshAllTransforms()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoadedGLTF::setActiveAnimation(int index, bool resetTime)
|
void LoadedGLTF::setActiveAnimation(AnimationState &state, int index, bool resetTime)
|
||||||
{
|
{
|
||||||
if (animations.empty())
|
if (animations.empty())
|
||||||
{
|
{
|
||||||
activeAnimation = -1;
|
state.activeAnimation = -1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index < 0 || index >= static_cast<int>(animations.size()))
|
if (index < 0)
|
||||||
|
{
|
||||||
|
state.activeAnimation = -1;
|
||||||
|
if (resetTime)
|
||||||
|
{
|
||||||
|
state.animationTime = 0.0f;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= static_cast<int>(animations.size()))
|
||||||
{
|
{
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
activeAnimation = index;
|
state.activeAnimation = index;
|
||||||
if (resetTime)
|
if (resetTime)
|
||||||
{
|
{
|
||||||
animationTime = 0.0f;
|
state.animationTime = 0.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoadedGLTF::setActiveAnimation(const std::string &name, bool resetTime)
|
void LoadedGLTF::setActiveAnimation(AnimationState &state, const std::string &name, bool resetTime)
|
||||||
{
|
{
|
||||||
for (size_t i = 0; i < animations.size(); ++i)
|
for (size_t i = 0; i < animations.size(); ++i)
|
||||||
{
|
{
|
||||||
if (animations[i].name == name)
|
if (animations[i].name == name)
|
||||||
{
|
{
|
||||||
setActiveAnimation(static_cast<int>(i), resetTime);
|
setActiveAnimation(state, static_cast<int>(i), resetTime);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoadedGLTF::updateAnimation(float dt)
|
void LoadedGLTF::updateAnimation(float dt, AnimationState &state)
|
||||||
{
|
{
|
||||||
if (animations.empty()) return;
|
if (animations.empty()) return;
|
||||||
if (activeAnimation < 0 || activeAnimation >= static_cast<int>(animations.size())) return;
|
if (state.activeAnimation < 0 || state.activeAnimation >= static_cast<int>(animations.size())) return;
|
||||||
if (dt <= 0.0f) return;
|
if (dt <= 0.0f) return;
|
||||||
|
|
||||||
Animation &clip = animations[activeAnimation];
|
Animation &clip = animations[state.activeAnimation];
|
||||||
if (clip.duration <= 0.0f) return;
|
if (clip.duration <= 0.0f) return;
|
||||||
|
|
||||||
animationTime += dt;
|
state.animationTime += dt;
|
||||||
if (animationLoop)
|
if (state.animationLoop)
|
||||||
{
|
{
|
||||||
animationTime = std::fmod(animationTime, clip.duration);
|
state.animationTime = std::fmod(state.animationTime, clip.duration);
|
||||||
if (animationTime < 0.0f)
|
if (state.animationTime < 0.0f)
|
||||||
{
|
{
|
||||||
animationTime += clip.duration;
|
state.animationTime += clip.duration;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (animationTime > clip.duration)
|
else if (state.animationTime > clip.duration)
|
||||||
{
|
{
|
||||||
animationTime = clip.duration;
|
state.animationTime = clip.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
float t = animationTime;
|
float t = state.animationTime;
|
||||||
|
|
||||||
for (auto &ch: clip.channels)
|
for (auto &ch: clip.channels)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -98,20 +98,24 @@ struct LoadedGLTF : public IRenderable
|
|||||||
std::vector<AnimationChannel> channels;
|
std::vector<AnimationChannel> channels;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct AnimationState
|
||||||
|
{
|
||||||
|
int activeAnimation = -1;
|
||||||
|
float animationTime = 0.f;
|
||||||
|
bool animationLoop = true;
|
||||||
|
};
|
||||||
|
|
||||||
std::vector<Animation> animations;
|
std::vector<Animation> animations;
|
||||||
int activeAnimation = -1;
|
|
||||||
float animationTime = 0.f;
|
|
||||||
bool animationLoop = true;
|
|
||||||
|
|
||||||
// Optional debug name (e.g., key used when loaded into SceneManager)
|
// Optional debug name (e.g., key used when loaded into SceneManager)
|
||||||
std::string debugName;
|
std::string debugName;
|
||||||
|
|
||||||
// Animation helpers
|
// Animation helpers
|
||||||
void updateAnimation(float dt);
|
void updateAnimation(float dt, AnimationState &state);
|
||||||
void refreshAllTransforms();
|
void refreshAllTransforms();
|
||||||
std::shared_ptr<Node> getNode(const std::string &name);
|
std::shared_ptr<Node> getNode(const std::string &name);
|
||||||
void setActiveAnimation(int index, bool resetTime = true);
|
void setActiveAnimation(AnimationState &state, int index, bool resetTime = true);
|
||||||
void setActiveAnimation(const std::string &name, bool resetTime = true);
|
void setActiveAnimation(AnimationState &state, const std::string &name, bool resetTime = true);
|
||||||
|
|
||||||
~LoadedGLTF()
|
~LoadedGLTF()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ void SceneManager::update_scene()
|
|||||||
mainDrawContext.OpaqueSurfaces.clear();
|
mainDrawContext.OpaqueSurfaces.clear();
|
||||||
mainDrawContext.TransparentSurfaces.clear();
|
mainDrawContext.TransparentSurfaces.clear();
|
||||||
mainDrawContext.nextID = 1;
|
mainDrawContext.nextID = 1;
|
||||||
|
mainDrawContext.gltfNodeLocalOverrides = nullptr;
|
||||||
|
|
||||||
mainCamera.update();
|
mainCamera.update();
|
||||||
|
|
||||||
@@ -140,30 +141,6 @@ void SceneManager::update_scene()
|
|||||||
dt = 0.1f;
|
dt = 0.1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advance glTF animations once per unique LoadedGLTF
|
|
||||||
if (dt > 0.f)
|
|
||||||
{
|
|
||||||
std::unordered_set<LoadedGLTF *> animatedScenes;
|
|
||||||
|
|
||||||
auto updateSceneAnim = [&](std::shared_ptr<LoadedGLTF> &scene) {
|
|
||||||
if (!scene) return;
|
|
||||||
LoadedGLTF *ptr = scene.get();
|
|
||||||
if (animatedScenes.insert(ptr).second)
|
|
||||||
{
|
|
||||||
ptr->updateAnimation(dt);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (auto &[name, scene] : loadedScenes)
|
|
||||||
{
|
|
||||||
updateSceneAnim(scene);
|
|
||||||
}
|
|
||||||
for (auto &[name, inst] : dynamicGLTFInstances)
|
|
||||||
{
|
|
||||||
updateSceneAnim(inst.scene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto tagOwner = [&](RenderObject::OwnerType type, const std::string &name,
|
auto tagOwner = [&](RenderObject::OwnerType type, const std::string &name,
|
||||||
size_t opaqueBegin, size_t transpBegin)
|
size_t opaqueBegin, size_t transpBegin)
|
||||||
{
|
{
|
||||||
@@ -179,29 +156,57 @@ void SceneManager::update_scene()
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Draw all loaded GLTF scenes (static world)
|
// Draw all loaded GLTF scenes (static world), advancing their independent animation states.
|
||||||
for (auto &[name, scene] : loadedScenes)
|
for (auto &[name, scene] : loadedScenes)
|
||||||
{
|
{
|
||||||
if (scene)
|
if (!scene)
|
||||||
{
|
{
|
||||||
const size_t opaqueStart = mainDrawContext.OpaqueSurfaces.size();
|
continue;
|
||||||
const size_t transpStart = mainDrawContext.TransparentSurfaces.size();
|
|
||||||
scene->Draw(glm::mat4{1.f}, mainDrawContext);
|
|
||||||
tagOwner(RenderObject::OwnerType::StaticGLTF, name, opaqueStart, transpStart);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advance this scene's animation state (independent of instances).
|
||||||
|
if (dt > 0.f)
|
||||||
|
{
|
||||||
|
auto &animState = sceneAnimations[name];
|
||||||
|
scene->updateAnimation(dt, animState);
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t opaqueStart = mainDrawContext.OpaqueSurfaces.size();
|
||||||
|
const size_t transpStart = mainDrawContext.TransparentSurfaces.size();
|
||||||
|
mainDrawContext.gltfNodeLocalOverrides = nullptr;
|
||||||
|
scene->Draw(glm::mat4{1.f}, mainDrawContext);
|
||||||
|
mainDrawContext.gltfNodeLocalOverrides = nullptr;
|
||||||
|
tagOwner(RenderObject::OwnerType::StaticGLTF, name, opaqueStart, transpStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
// dynamic GLTF instances
|
// dynamic GLTF instances (each with its own animation state)
|
||||||
for (const auto &kv: dynamicGLTFInstances)
|
for (auto &kv : dynamicGLTFInstances)
|
||||||
{
|
{
|
||||||
const GLTFInstance &inst = kv.second;
|
GLTFInstance &inst = kv.second;
|
||||||
if (inst.scene)
|
if (!inst.scene)
|
||||||
{
|
{
|
||||||
const size_t opaqueStart = mainDrawContext.OpaqueSurfaces.size();
|
continue;
|
||||||
const size_t transpStart = mainDrawContext.TransparentSurfaces.size();
|
|
||||||
inst.scene->Draw(inst.transform, mainDrawContext);
|
|
||||||
tagOwner(RenderObject::OwnerType::GLTFInstance, kv.first, opaqueStart, transpStart);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dt > 0.f)
|
||||||
|
{
|
||||||
|
inst.scene->updateAnimation(dt, inst.animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t opaqueStart = mainDrawContext.OpaqueSurfaces.size();
|
||||||
|
const size_t transpStart = mainDrawContext.TransparentSurfaces.size();
|
||||||
|
// Enable per-instance node pose overrides while drawing this instance.
|
||||||
|
if (!inst.nodeLocalOverrides.empty())
|
||||||
|
{
|
||||||
|
mainDrawContext.gltfNodeLocalOverrides = &inst.nodeLocalOverrides;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mainDrawContext.gltfNodeLocalOverrides = nullptr;
|
||||||
|
}
|
||||||
|
inst.scene->Draw(inst.transform, mainDrawContext);
|
||||||
|
mainDrawContext.gltfNodeLocalOverrides = nullptr;
|
||||||
|
tagOwner(RenderObject::OwnerType::GLTFInstance, kv.first, opaqueStart, transpStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default primitives are added as dynamic instances by the engine.
|
// Default primitives are added as dynamic instances by the engine.
|
||||||
@@ -371,7 +376,21 @@ void SceneManager::loadScene(const std::string &name, std::shared_ptr<LoadedGLTF
|
|||||||
{
|
{
|
||||||
scene->debugName = name;
|
scene->debugName = name;
|
||||||
}
|
}
|
||||||
loadedScenes[name] = std::move(scene);
|
loadedScenes[name] = scene;
|
||||||
|
|
||||||
|
// Initialize default animation state for this named scene (play first clip if present).
|
||||||
|
if (scene && !scene->animations.empty())
|
||||||
|
{
|
||||||
|
LoadedGLTF::AnimationState st{};
|
||||||
|
st.activeAnimation = 0;
|
||||||
|
st.animationTime = 0.0f;
|
||||||
|
st.animationLoop = true;
|
||||||
|
sceneAnimations[name] = st;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sceneAnimations.erase(name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<LoadedGLTF> SceneManager::getScene(const std::string &name)
|
std::shared_ptr<LoadedGLTF> SceneManager::getScene(const std::string &name)
|
||||||
@@ -400,6 +419,7 @@ void SceneManager::cleanup()
|
|||||||
// Drop our references to GLTF scenes. Their destructors call clearAll()
|
// Drop our references to GLTF scenes. Their destructors call clearAll()
|
||||||
// exactly once to release GPU resources.
|
// exactly once to release GPU resources.
|
||||||
loadedScenes.clear();
|
loadedScenes.clear();
|
||||||
|
sceneAnimations.clear();
|
||||||
loadedNodes.clear();
|
loadedNodes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,7 +473,16 @@ void SceneManager::addGLTFInstance(const std::string &name, std::shared_ptr<Load
|
|||||||
fmt::println("[SceneManager] addGLTFInstance '{}' (scene='{}')",
|
fmt::println("[SceneManager] addGLTFInstance '{}' (scene='{}')",
|
||||||
name,
|
name,
|
||||||
scene->debugName.empty() ? "<unnamed>" : scene->debugName.c_str());
|
scene->debugName.empty() ? "<unnamed>" : scene->debugName.c_str());
|
||||||
dynamicGLTFInstances[name] = GLTFInstance{std::move(scene), transform};
|
GLTFInstance inst{};
|
||||||
|
inst.scene = std::move(scene);
|
||||||
|
inst.transform = transform;
|
||||||
|
if (inst.scene && !inst.scene->animations.empty())
|
||||||
|
{
|
||||||
|
inst.animation.activeAnimation = 0;
|
||||||
|
inst.animation.animationTime = 0.0f;
|
||||||
|
inst.animation.animationLoop = true;
|
||||||
|
}
|
||||||
|
dynamicGLTFInstances[name] = std::move(inst);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SceneManager::removeGLTFInstance(const std::string &name)
|
bool SceneManager::removeGLTFInstance(const std::string &name)
|
||||||
@@ -519,6 +548,71 @@ void SceneManager::clearGLTFInstances()
|
|||||||
pendingGLTFRelease.size());
|
pendingGLTFRelease.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SceneManager::setGLTFInstanceNodeOffset(const std::string &instanceName,
|
||||||
|
const std::string &nodeName,
|
||||||
|
const glm::mat4 &offset)
|
||||||
|
{
|
||||||
|
auto it = dynamicGLTFInstances.find(instanceName);
|
||||||
|
if (it == dynamicGLTFInstances.end())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
GLTFInstance &inst = it->second;
|
||||||
|
if (!inst.scene)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nodePtr = inst.scene->getNode(nodeName);
|
||||||
|
if (!nodePtr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inst.nodeLocalOverrides[nodePtr.get()] = offset;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SceneManager::clearGLTFInstanceNodeOffset(const std::string &instanceName,
|
||||||
|
const std::string &nodeName)
|
||||||
|
{
|
||||||
|
auto it = dynamicGLTFInstances.find(instanceName);
|
||||||
|
if (it == dynamicGLTFInstances.end())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
GLTFInstance &inst = it->second;
|
||||||
|
if (!inst.scene)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nodePtr = inst.scene->getNode(nodeName);
|
||||||
|
if (!nodePtr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ovIt = inst.nodeLocalOverrides.find(nodePtr.get());
|
||||||
|
if (ovIt == inst.nodeLocalOverrides.end())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inst.nodeLocalOverrides.erase(ovIt);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneManager::clearGLTFInstanceNodeOffsets(const std::string &instanceName)
|
||||||
|
{
|
||||||
|
auto it = dynamicGLTFInstances.find(instanceName);
|
||||||
|
if (it == dynamicGLTFInstances.end())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
it->second.nodeLocalOverrides.clear();
|
||||||
|
}
|
||||||
|
|
||||||
bool SceneManager::setSceneAnimation(const std::string &sceneName, int animationIndex, bool resetTime)
|
bool SceneManager::setSceneAnimation(const std::string &sceneName, int animationIndex, bool resetTime)
|
||||||
{
|
{
|
||||||
auto it = loadedScenes.find(sceneName);
|
auto it = loadedScenes.find(sceneName);
|
||||||
@@ -527,7 +621,8 @@ bool SceneManager::setSceneAnimation(const std::string &sceneName, int animation
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
it->second->setActiveAnimation(animationIndex, resetTime);
|
auto &animState = sceneAnimations[sceneName];
|
||||||
|
it->second->setActiveAnimation(animState, animationIndex, resetTime);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,7 +634,8 @@ bool SceneManager::setSceneAnimation(const std::string &sceneName, const std::st
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
it->second->setActiveAnimation(animationName, resetTime);
|
auto &animState = sceneAnimations[sceneName];
|
||||||
|
it->second->setActiveAnimation(animState, animationName, resetTime);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,7 +647,8 @@ bool SceneManager::setSceneAnimationLoop(const std::string &sceneName, bool loop
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
it->second->animationLoop = loop;
|
auto &animState = sceneAnimations[sceneName];
|
||||||
|
animState.animationLoop = loop;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -563,7 +660,8 @@ bool SceneManager::setGLTFInstanceAnimation(const std::string &instanceName, int
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
it->second.scene->setActiveAnimation(animationIndex, resetTime);
|
LoadedGLTF::AnimationState &animState = it->second.animation;
|
||||||
|
it->second.scene->setActiveAnimation(animState, animationIndex, resetTime);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,7 +673,8 @@ bool SceneManager::setGLTFInstanceAnimation(const std::string &instanceName, con
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
it->second.scene->setActiveAnimation(animationName, resetTime);
|
LoadedGLTF::AnimationState &animState = it->second.animation;
|
||||||
|
it->second.scene->setActiveAnimation(animState, animationName, resetTime);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,6 +686,6 @@ bool SceneManager::setGLTFInstanceAnimationLoop(const std::string &instanceName,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
it->second.scene->animationLoop = loop;
|
it->second.animation.animationLoop = loop;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ struct DrawContext
|
|||||||
std::vector<RenderObject> TransparentSurfaces;
|
std::vector<RenderObject> TransparentSurfaces;
|
||||||
// Monotonic counter used to assign stable per-frame object IDs.
|
// Monotonic counter used to assign stable per-frame object IDs.
|
||||||
uint32_t nextID = 1;
|
uint32_t nextID = 1;
|
||||||
|
// Optional per-instance glTF node local overrides (additive layer in local space).
|
||||||
|
// When non-null, MeshNode::Draw will rebuild world transforms using these offsets.
|
||||||
|
const std::unordered_map<const Node*, glm::mat4> *gltfNodeLocalOverrides = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SceneManager
|
class SceneManager
|
||||||
@@ -105,6 +108,10 @@ public:
|
|||||||
{
|
{
|
||||||
std::shared_ptr<LoadedGLTF> scene;
|
std::shared_ptr<LoadedGLTF> scene;
|
||||||
glm::mat4 transform{1.f};
|
glm::mat4 transform{1.f};
|
||||||
|
LoadedGLTF::AnimationState animation;
|
||||||
|
// Per-instance local-space pose offsets for nodes in this glTF scene.
|
||||||
|
// The offset matrix is post-multiplied onto the node's localTransform.
|
||||||
|
std::unordered_map<const Node*, glm::mat4> nodeLocalOverrides;
|
||||||
};
|
};
|
||||||
|
|
||||||
void addGLTFInstance(const std::string &name, std::shared_ptr<LoadedGLTF> scene,
|
void addGLTFInstance(const std::string &name, std::shared_ptr<LoadedGLTF> scene,
|
||||||
@@ -113,6 +120,14 @@ public:
|
|||||||
bool getGLTFInstanceTransform(const std::string &name, glm::mat4 &outTransform);
|
bool getGLTFInstanceTransform(const std::string &name, glm::mat4 &outTransform);
|
||||||
bool setGLTFInstanceTransform(const std::string &name, const glm::mat4 &transform);
|
bool setGLTFInstanceTransform(const std::string &name, const glm::mat4 &transform);
|
||||||
void clearGLTFInstances();
|
void clearGLTFInstances();
|
||||||
|
// Per-instance glTF node pose overrides (local-space, layered on top of animation/base TRS).
|
||||||
|
// 'offset' is post-multiplied onto the node's localTransform for this instance only.
|
||||||
|
bool setGLTFInstanceNodeOffset(const std::string &instanceName,
|
||||||
|
const std::string &nodeName,
|
||||||
|
const glm::mat4 &offset);
|
||||||
|
bool clearGLTFInstanceNodeOffset(const std::string &instanceName,
|
||||||
|
const std::string &nodeName);
|
||||||
|
void clearGLTFInstanceNodeOffsets(const std::string &instanceName);
|
||||||
|
|
||||||
// Animation control helpers (glTF)
|
// Animation control helpers (glTF)
|
||||||
// Note: a LoadedGLTF may be shared by multiple instances; changing
|
// Note: a LoadedGLTF may be shared by multiple instances; changing
|
||||||
@@ -167,6 +182,8 @@ private:
|
|||||||
std::vector<PointLight> pointLights;
|
std::vector<PointLight> pointLights;
|
||||||
|
|
||||||
std::unordered_map<std::string, std::shared_ptr<LoadedGLTF> > loadedScenes;
|
std::unordered_map<std::string, std::shared_ptr<LoadedGLTF> > loadedScenes;
|
||||||
|
// Per-named static glTF scene animation state (independent of instances).
|
||||||
|
std::unordered_map<std::string, LoadedGLTF::AnimationState> sceneAnimations;
|
||||||
std::unordered_map<std::string, std::shared_ptr<Node> > loadedNodes;
|
std::unordered_map<std::string, std::shared_ptr<Node> > loadedNodes;
|
||||||
std::unordered_map<std::string, MeshInstance> dynamicMeshInstances;
|
std::unordered_map<std::string, MeshInstance> dynamicMeshInstances;
|
||||||
std::unordered_map<std::string, GLTFInstance> dynamicGLTFInstances;
|
std::unordered_map<std::string, GLTFInstance> dynamicGLTFInstances;
|
||||||
|
|||||||
Reference in New Issue
Block a user