#include "vk_scene.h" #include #include #include #include "scene/planet/planet_system.h" #include "core/device/swapchain.h" #include "core/context.h" #include "core/config.h" #include "glm/gtx/transform.hpp" #include #include "glm/gtx/norm.inl" #include "glm/gtx/compatibility.hpp" #include #include #include #include "core/frame/resources.h" #include "core/config.h" #include SceneManager::SceneManager() = default; SceneManager::~SceneManager() { fmt::println("[SceneManager] dtor: loadedScenes={} dynamicGLTFInstances={} pendingGLTFRelease={}", loadedScenes.size(), dynamicGLTFInstances.size(), pendingGLTFRelease.size()); } void SceneManager::addPointLight(const PointLight &light) { pointLights.push_back(light); } void SceneManager::clearPointLights() { pointLights.clear(); } bool SceneManager::getPointLight(size_t index, PointLight &outLight) const { if (index >= pointLights.size()) { return false; } outLight = pointLights[index]; return true; } bool SceneManager::setPointLight(size_t index, const PointLight &light) { if (index >= pointLights.size()) { return false; } pointLights[index] = light; return true; } bool SceneManager::removePointLight(size_t index) { if (index >= pointLights.size()) { return false; } pointLights.erase(pointLights.begin() + index); return true; } void SceneManager::addSpotLight(const SpotLight &light) { spotLights.push_back(light); } void SceneManager::clearSpotLights() { spotLights.clear(); } bool SceneManager::getSpotLight(size_t index, SpotLight &outLight) const { if (index >= spotLights.size()) { return false; } outLight = spotLights[index]; return true; } bool SceneManager::setSpotLight(size_t index, const SpotLight &light) { if (index >= spotLights.size()) { return false; } spotLights[index] = light; return true; } bool SceneManager::removeSpotLight(size_t index) { if (index >= spotLights.size()) { return false; } spotLights.erase(spotLights.begin() + index); return true; } void SceneManager::init(EngineContext *context) { _context = context; mainCamera.position_world = WorldVec3(30.0, 0.0, 85.0); mainCamera.orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); sceneData.ambientColor = glm::vec4(0.1f, 0.1f, 0.1f, 1.0f); sceneData.sunlightDirection = glm::vec4(-0.2f, -1.0f, -0.3f, 1.0f); sceneData.sunlightColor = glm::vec4(1.0f, 1.0f, 1.0f, 3.0f); cameraRig.init(*this, mainCamera); _camera_position_local = world_to_local(mainCamera.position_world, _origin_world); _planetSystem = std::make_unique(); _planetSystem->init(_context); } void SceneManager::update_scene() { auto start = std::chrono::system_clock::now(); // Release any GLTF assets that were scheduled for safe destruction after GPU idle. // Defer actual destruction to the current frame's deletion queue so we wait // until the GPU has finished work that might still reference their resources. if (_context && _context->currentFrame) { if (!pendingGLTFRelease.empty()) { fmt::println("[SceneManager] update_scene: scheduling {} pending GLTF releases (hasContext={}, hasFrame={})", pendingGLTFRelease.size(), true, true); } for (auto &sp : pendingGLTFRelease) { auto keepAlive = sp; // copy to keep ref count in the lambda _context->currentFrame->_deletionQueue.push_function([keepAlive]() mutable { keepAlive.reset(); }); } } pendingGLTFRelease.clear(); mainDrawContext.OpaqueSurfaces.clear(); mainDrawContext.TransparentSurfaces.clear(); mainDrawContext.nextID = 1; mainDrawContext.gltfNodeLocalOverrides = nullptr; // Simple per-frame dt (seconds) for animations and camera behavior. auto now = std::chrono::steady_clock::now(); if (_lastFrameTime.time_since_epoch().count() == 0) { _lastFrameTime = now; } float dt = std::chrono::duration(now - _lastFrameTime).count(); _lastFrameTime = now; if (dt < 0.f) { dt = 0.f; } if (dt > 0.1f) { dt = 0.1f; } _deltaTime = dt; cameraRig.update(*this, mainCamera, dt); // Floating origin: keep render-local coordinates near (0,0,0) by shifting the origin // when the camera drifts too far in world space. if (_floating_origin_recenter_threshold > 0.0) { const WorldVec3 d = mainCamera.position_world - _origin_world; const double threshold2 = _floating_origin_recenter_threshold * _floating_origin_recenter_threshold; if (glm::length2(d) > threshold2) { const WorldVec3 newOrigin = (_floating_origin_snap_size > 0.0) ? snap_world(mainCamera.position_world, _floating_origin_snap_size) : mainCamera.position_world; _origin_world = newOrigin; } } _camera_position_local = world_to_local(mainCamera.position_world, _origin_world); auto tagOwner = [&](RenderObject::OwnerType type, const std::string &name, size_t opaqueBegin, size_t transpBegin) { for (size_t i = opaqueBegin; i < mainDrawContext.OpaqueSurfaces.size(); ++i) { mainDrawContext.OpaqueSurfaces[i].ownerType = type; mainDrawContext.OpaqueSurfaces[i].ownerName = name; } for (size_t i = transpBegin; i < mainDrawContext.TransparentSurfaces.size(); ++i) { mainDrawContext.TransparentSurfaces[i].ownerType = type; mainDrawContext.TransparentSurfaces[i].ownerName = name; } }; // Root transform to shift "world space" float scenes into render-local space. // Any object that is authored in world coordinates (float) should be offset by -origin. const glm::mat4 world_to_local_root = glm::translate(glm::mat4{1.f}, world_to_local(WorldVec3(0.0, 0.0, 0.0), _origin_world)); // Draw all loaded GLTF scenes (static world), advancing their independent animation states. for (auto &[name, scene] : loadedScenes) { if (!scene) { continue; } // 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(world_to_local_root, mainDrawContext); mainDrawContext.gltfNodeLocalOverrides = nullptr; tagOwner(RenderObject::OwnerType::StaticGLTF, name, opaqueStart, transpStart); } // dynamic GLTF instances (each with its own animation state) for (auto &kv : dynamicGLTFInstances) { GLTFInstance &inst = kv.second; if (!inst.scene) { continue; } 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; } glm::vec3 tLocal = world_to_local(inst.translation_world, _origin_world); glm::mat4 instanceTransform = make_trs_matrix(tLocal, inst.rotation, inst.scale); inst.scene->Draw(instanceTransform, mainDrawContext); mainDrawContext.gltfNodeLocalOverrides = nullptr; tagOwner(RenderObject::OwnerType::GLTFInstance, kv.first, opaqueStart, transpStart); } // Default primitives are added as dynamic instances by the engine. // dynamic mesh instances for (const auto &kv: dynamicMeshInstances) { const MeshInstance &inst = kv.second; if (!inst.mesh || inst.mesh->surfaces.empty()) continue; glm::vec3 tLocal = world_to_local(inst.translation_world, _origin_world); glm::mat4 instanceTransform = make_trs_matrix(tLocal, inst.rotation, inst.scale); uint32_t surfaceIndex = 0; for (const auto &surf: inst.mesh->surfaces) { RenderObject obj{}; obj.indexCount = surf.count; obj.firstIndex = surf.startIndex; obj.indexBuffer = inst.mesh->meshBuffers.indexBuffer.buffer; obj.vertexBuffer = inst.mesh->meshBuffers.vertexBuffer.buffer; obj.vertexBufferAddress = inst.mesh->meshBuffers.vertexBufferAddress; obj.material = &surf.material->data; obj.bounds = surf.bounds; if (inst.boundsTypeOverride.has_value()) { obj.bounds.type = *inst.boundsTypeOverride; } obj.transform = instanceTransform; obj.sourceMesh = inst.mesh.get(); obj.surfaceIndex = surfaceIndex++; obj.objectID = mainDrawContext.nextID++; obj.ownerType = RenderObject::OwnerType::MeshInstance; obj.ownerName = kv.first; if (obj.material->passType == MaterialPass::Transparent) { mainDrawContext.TransparentSurfaces.push_back(obj); } else { mainDrawContext.OpaqueSurfaces.push_back(obj); } } } if (_planetSystem) { _planetSystem->update_and_emit(*this, mainDrawContext); } glm::mat4 view = mainCamera.getViewMatrix(_camera_position_local); // Use reversed infinite-Z projection (right-handed, -Z forward) to avoid far-plane clipping // on very large scenes. Vulkan clip space is 0..1 (GLM_FORCE_DEPTH_ZERO_TO_ONE) and requires Y flip. auto makeReversedInfinitePerspective = [](float fovyRadians, float aspect, float zNear) { // Column-major matrix; indices are [column][row] float f = 1.0f / tanf(fovyRadians * 0.5f); glm::mat4 m(0.0f); m[0][0] = f / aspect; m[1][1] = f; m[2][2] = 0.0f; m[2][3] = -1.0f; // w = -z_eye (right-handed) m[3][2] = zNear; // maps near -> 1, far -> 0 (reversed-Z) return m; }; // Keep projection FOV in sync with the camera so that CPU ray picking // matches what is rendered inside the fixed logical render area (letterboxed). const float fov = glm::radians(mainCamera.fovDegrees); // Derive aspect ratio from the fixed logical render size instead of the window/swapchain. VkExtent2D logicalExtent{ kRenderWidth, kRenderHeight }; if (_context) { VkExtent2D ctxExtent = _context->getLogicalRenderExtent(); if (ctxExtent.width > 0 && ctxExtent.height > 0) { logicalExtent = ctxExtent; } } const float aspect = static_cast(logicalExtent.width) / static_cast(logicalExtent.height); const float nearPlane = 0.1f; glm::mat4 projection = makeReversedInfinitePerspective(fov, aspect, nearPlane); // Vulkan NDC has inverted Y. projection[1][1] *= -1.0f; sceneData.view = view; sceneData.proj = projection; sceneData.viewproj = projection * view; // Clipmap shadow setup (directional). Each level i covers a square region // around the camera in the light's XY plane with radius R_i = R0 * 2^i. // The region center is snapped to the light-space texel grid for stability. static const float kAheadBlend[kShadowCascadeCount] = {0.2f, 0.5f, 0.75f, 1.0f}; { const glm::mat4 invView = glm::inverse(view); const glm::vec3 camPos = glm::vec3(invView[3]); const glm::vec3 camFwd = -glm::vec3(invView[2]); glm::vec3 L = glm::normalize(-glm::vec3(sceneData.sunlightDirection)); if (glm::length(L) < 1e-5f) L = glm::vec3(0.0f, -1.0f, 0.0f); const glm::vec3 worldUp(0.0f, 1.0f, 0.0f); glm::vec3 right = glm::cross(L, worldUp); if (glm::length2(right) < 1e-6f) right = glm::vec3(1, 0, 0); right = glm::normalize(right); glm::vec3 up = glm::normalize(glm::cross(right, L)); auto level_radius = [](int level) { return kShadowClipBaseRadius * powf(2.0f, float(level)); }; sceneData.cascadeSplitsView = glm::vec4( level_radius(0), level_radius(1), level_radius(2), level_radius(3)); for (int ci = 0; ci < kShadowCascadeCount; ++ci) { const float radius = level_radius(ci); const float cover = radius * kShadowCascadeRadiusScale + kShadowCascadeRadiusMargin; const float ahead = radius * 0.5; const float fu = glm::dot(camFwd, right); const float fv = glm::dot(camFwd, up); const glm::vec3 aheadXY = right * (fu * ahead) + up * (fv * ahead); const float u = glm::dot(camPos + aheadXY, right) + fu * ahead; const float v = glm::dot(camPos + aheadXY, up) + fv * ahead; const float texel = (2.0f * cover) / float(kShadowMapResolution); const float uSnapped = floorf(u / texel) * texel; const float vSnapped = floorf(v / texel) * texel; const float du = uSnapped - u; const float dv = vSnapped - v; const glm::vec3 center = camPos + aheadXY * kAheadBlend[ci] + right * du + up * dv; const float pullback = glm::max(kShadowClipPullbackMin, cover * kShadowClipPullbackFactor); const glm::vec3 eye = center - L * pullback; const glm::mat4 V = glm::lookAtRH(eye, center, up); const float zNear = 0.2f; const float zFar = pullback + cover * kShadowClipForwardFactor + kShadowClipZPadding; const glm::mat4 P = glm::orthoRH_ZO(-cover, cover, -cover, cover, zNear, zFar); const glm::mat4 lightVP = P * V; sceneData.lightViewProjCascades[ci] = lightVP; if (ci == 0) { sceneData.lightViewProj = lightVP; } } } // Publish shadow/RT settings to SceneData if (_context) { const auto &ss = _context->shadowSettings; // RT shadows are considered active only when shadows are enabled and // a hybrid/RT shadow mode is selected (mode != 0). const uint32_t rtEnabled = (ss.enabled && ss.mode != 0u) ? 1u : 0u; const uint32_t reflMode = _context->reflectionMode; // rtOptions.x = RT shadows enabled, y = cascade mask, z = shadow mode, w = reflection mode (SSR/RT) sceneData.rtOptions = glm::uvec4(rtEnabled, ss.hybridRayCascadesMask, ss.mode, reflMode); // rtParams.x = N·L threshold for hybrid shadows // rtParams.y = shadows enabled flag (1.0 = on, 0.0 = off) sceneData.rtParams = glm::vec4(ss.hybridRayNoLThreshold, ss.enabled ? 1.0f : 0.0f, 0.0f, 0.0f); } // Fill punctual lights into GPUSceneData const uint32_t lightCount = static_cast(std::min(pointLights.size(), static_cast(kMaxPunctualLights))); for (uint32_t i = 0; i < lightCount; ++i) { const PointLight &pl = pointLights[i]; glm::vec3 posLocal = world_to_local(pl.position_world, _origin_world); sceneData.punctualLights[i].position_radius = glm::vec4(posLocal, pl.radius); sceneData.punctualLights[i].color_intensity = glm::vec4(pl.color, pl.intensity); } for (uint32_t i = lightCount; i < kMaxPunctualLights; ++i) { sceneData.punctualLights[i].position_radius = glm::vec4(0.0f); sceneData.punctualLights[i].color_intensity = glm::vec4(0.0f); } // Fill spot lights into GPUSceneData const uint32_t spotCount = static_cast(std::min(spotLights.size(), static_cast(kMaxSpotLights))); for (uint32_t i = 0; i < spotCount; ++i) { const SpotLight &sl = spotLights[i]; glm::vec3 posLocal = world_to_local(sl.position_world, _origin_world); glm::vec3 dir = sl.direction; const float dirLen2 = glm::length2(dir); if (dirLen2 > 1.0e-8f) { dir *= 1.0f / std::sqrt(dirLen2); } else { dir = glm::vec3(0.0f, -1.0f, 0.0f); } const float radius = std::max(sl.radius, 0.0001f); const float innerDeg = std::clamp(sl.inner_angle_deg, 0.0f, 89.0f); const float outerDeg = std::clamp(sl.outer_angle_deg, innerDeg, 89.9f); const float cosInner = glm::cos(glm::radians(innerDeg)); const float cosOuter = glm::cos(glm::radians(outerDeg)); sceneData.spotLights[i].position_radius = glm::vec4(posLocal, radius); sceneData.spotLights[i].direction_cos_outer = glm::vec4(dir, cosOuter); sceneData.spotLights[i].color_intensity = glm::vec4(sl.color, sl.intensity); sceneData.spotLights[i].cone = glm::vec4(cosInner, 0.0f, 0.0f, 0.0f); } for (uint32_t i = spotCount; i < kMaxSpotLights; ++i) { sceneData.spotLights[i].position_radius = glm::vec4(0.0f); sceneData.spotLights[i].direction_cos_outer = glm::vec4(0.0f); sceneData.spotLights[i].color_intensity = glm::vec4(0.0f); sceneData.spotLights[i].cone = glm::vec4(0.0f); } sceneData.lightCounts = glm::uvec4(lightCount, spotCount, 0u, 0u); auto end = std::chrono::system_clock::now(); auto elapsed = std::chrono::duration_cast(end - start); stats.scene_update_time = elapsed.count() / 1000.f; } void SceneManager::loadScene(const std::string &name, std::shared_ptr scene) { if (scene) { scene->debugName = name; } 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 SceneManager::getScene(const std::string &name) { auto it = loadedScenes.find(name); return (it != loadedScenes.end()) ? it->second : nullptr; } void SceneManager::cleanup() { // Explicitly clear dynamic instances first to drop any extra shared_ptrs // that could keep GPU resources alive. clearMeshInstances(); clearGLTFInstances(); // On engine shutdown we know VulkanEngine::cleanup() has already called // vkDeviceWaitIdle(), so it is safe to destroy all remaining GLTF scenes // immediately instead of deferring them through pendingGLTFRelease. if (!pendingGLTFRelease.empty()) { fmt::println("[SceneManager] cleanup: forcing {} pending GLTF releases before shutdown", pendingGLTFRelease.size()); pendingGLTFRelease.clear(); // drop strong refs → ~LoadedGLTF::clearAll() runs } // Drop our references to GLTF scenes. Their destructors call clearAll() // exactly once to release GPU resources. loadedScenes.clear(); sceneAnimations.clear(); loadedNodes.clear(); } void SceneManager::addMeshInstance(const std::string &name, std::shared_ptr mesh, const glm::mat4 &transform, std::optional boundsType) { if (!mesh) return; MeshInstance inst{}; inst.mesh = std::move(mesh); glm::vec3 t{}; decompose_trs_matrix(transform, t, inst.rotation, inst.scale); inst.translation_world = WorldVec3(t); inst.boundsTypeOverride = boundsType; dynamicMeshInstances[name] = std::move(inst); } bool SceneManager::getMeshInstanceTransform(const std::string &name, glm::mat4 &outTransform) { auto it = dynamicMeshInstances.find(name); if (it == dynamicMeshInstances.end()) { return false; } const MeshInstance &inst = it->second; outTransform = make_trs_matrix(glm::vec3(inst.translation_world), inst.rotation, inst.scale); return true; } bool SceneManager::setMeshInstanceTransform(const std::string &name, const glm::mat4 &transform) { auto it = dynamicMeshInstances.find(name); if (it == dynamicMeshInstances.end()) { return false; } MeshInstance &inst = it->second; glm::vec3 t{}; decompose_trs_matrix(transform, t, inst.rotation, inst.scale); inst.translation_world = WorldVec3(t); return true; } bool SceneManager::getMeshInstanceTransformLocal(const std::string &name, glm::mat4 &outTransformLocal) const { auto it = dynamicMeshInstances.find(name); if (it == dynamicMeshInstances.end()) { return false; } const MeshInstance &inst = it->second; glm::vec3 tLocal = world_to_local(inst.translation_world, _origin_world); outTransformLocal = make_trs_matrix(tLocal, inst.rotation, inst.scale); return true; } bool SceneManager::setMeshInstanceTransformLocal(const std::string &name, const glm::mat4 &transformLocal) { auto it = dynamicMeshInstances.find(name); if (it == dynamicMeshInstances.end()) { return false; } MeshInstance &inst = it->second; glm::vec3 tLocal{}; decompose_trs_matrix(transformLocal, tLocal, inst.rotation, inst.scale); inst.translation_world = local_to_world(tLocal, _origin_world); return true; } bool SceneManager::getMeshInstanceTRSWorld(const std::string &name, WorldVec3 &outTranslationWorld, glm::quat &outRotation, glm::vec3 &outScale) const { auto it = dynamicMeshInstances.find(name); if (it == dynamicMeshInstances.end()) { return false; } const MeshInstance &inst = it->second; outTranslationWorld = inst.translation_world; outRotation = inst.rotation; outScale = inst.scale; return true; } bool SceneManager::setMeshInstanceTRSWorld(const std::string &name, const WorldVec3 &translationWorld, const glm::quat &rotation, const glm::vec3 &scale) { auto it = dynamicMeshInstances.find(name); if (it == dynamicMeshInstances.end()) { return false; } MeshInstance &inst = it->second; inst.translation_world = translationWorld; inst.rotation = rotation; inst.scale = scale; return true; } bool SceneManager::removeMeshInstance(const std::string &name) { return dynamicMeshInstances.erase(name) > 0; } void SceneManager::clearMeshInstances() { dynamicMeshInstances.clear(); } void SceneManager::addGLTFInstance(const std::string &name, std::shared_ptr scene, const glm::mat4 &transform) { if (!scene) return; fmt::println("[SceneManager] addGLTFInstance '{}' (scene='{}')", name, scene->debugName.empty() ? "" : scene->debugName.c_str()); GLTFInstance inst{}; inst.scene = std::move(scene); glm::vec3 t{}; decompose_trs_matrix(transform, t, inst.rotation, inst.scale); inst.translation_world = WorldVec3(t); 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) { auto it = dynamicGLTFInstances.find(name); if (it == dynamicGLTFInstances.end()) return false; // Defer destruction until after the next frame fence (update_scene). if (it->second.scene) { if (_context && _context->currentFrame) { auto keepAlive = it->second.scene; fmt::println("[SceneManager] removeGLTFInstance '{}' scheduling deferred destroy (scene='{}')", name, keepAlive && !keepAlive->debugName.empty() ? keepAlive->debugName.c_str() : ""); _context->currentFrame->_deletionQueue.push_function([keepAlive]() mutable { keepAlive.reset(); }); } else { // Fallback: stash until we have a frame to attach the destruction to. pendingGLTFRelease.push_back(it->second.scene); } } dynamicGLTFInstances.erase(it); return true; } std::shared_ptr SceneManager::getGLTFInstanceScene(const std::string &instanceName) const { auto it = dynamicGLTFInstances.find(instanceName); if (it != dynamicGLTFInstances.end()) { return it->second.scene; } return nullptr; } bool SceneManager::getGLTFInstanceTransform(const std::string &name, glm::mat4 &outTransform) { auto it = dynamicGLTFInstances.find(name); if (it == dynamicGLTFInstances.end()) { return false; } const GLTFInstance &inst = it->second; outTransform = make_trs_matrix(glm::vec3(inst.translation_world), inst.rotation, inst.scale); return true; } bool SceneManager::setGLTFInstanceTransform(const std::string &name, const glm::mat4 &transform) { auto it = dynamicGLTFInstances.find(name); if (it == dynamicGLTFInstances.end()) return false; GLTFInstance &inst = it->second; glm::vec3 t{}; decompose_trs_matrix(transform, t, inst.rotation, inst.scale); inst.translation_world = WorldVec3(t); return true; } bool SceneManager::getGLTFInstanceTransformLocal(const std::string &name, glm::mat4 &outTransformLocal) const { auto it = dynamicGLTFInstances.find(name); if (it == dynamicGLTFInstances.end()) { return false; } const GLTFInstance &inst = it->second; glm::vec3 tLocal = world_to_local(inst.translation_world, _origin_world); outTransformLocal = make_trs_matrix(tLocal, inst.rotation, inst.scale); return true; } bool SceneManager::setGLTFInstanceTransformLocal(const std::string &name, const glm::mat4 &transformLocal) { auto it = dynamicGLTFInstances.find(name); if (it == dynamicGLTFInstances.end()) { return false; } GLTFInstance &inst = it->second; glm::vec3 tLocal{}; decompose_trs_matrix(transformLocal, tLocal, inst.rotation, inst.scale); inst.translation_world = local_to_world(tLocal, _origin_world); return true; } bool SceneManager::getGLTFInstanceTRSWorld(const std::string &name, WorldVec3 &outTranslationWorld, glm::quat &outRotation, glm::vec3 &outScale) const { auto it = dynamicGLTFInstances.find(name); if (it == dynamicGLTFInstances.end()) { return false; } const GLTFInstance &inst = it->second; outTranslationWorld = inst.translation_world; outRotation = inst.rotation; outScale = inst.scale; return true; } bool SceneManager::setGLTFInstanceTRSWorld(const std::string &name, const WorldVec3 &translationWorld, const glm::quat &rotation, const glm::vec3 &scale) { auto it = dynamicGLTFInstances.find(name); if (it == dynamicGLTFInstances.end()) { return false; } GLTFInstance &inst = it->second; inst.translation_world = translationWorld; inst.rotation = rotation; inst.scale = scale; return true; } void SceneManager::clearGLTFInstances() { fmt::println("[SceneManager] clearGLTFInstances: dynamicGLTFInstances={} pendingBefore={}", dynamicGLTFInstances.size(), pendingGLTFRelease.size()); for (auto &kv : dynamicGLTFInstances) { if (kv.second.scene) { pendingGLTFRelease.push_back(kv.second.scene); } } dynamicGLTFInstances.clear(); fmt::println("[SceneManager] clearGLTFInstances: pendingAfter={}", 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) { auto it = loadedScenes.find(sceneName); if (it == loadedScenes.end() || !it->second) { return false; } auto &animState = sceneAnimations[sceneName]; it->second->setActiveAnimation(animState, animationIndex, resetTime); return true; } bool SceneManager::setSceneAnimation(const std::string &sceneName, const std::string &animationName, bool resetTime) { auto it = loadedScenes.find(sceneName); if (it == loadedScenes.end() || !it->second) { return false; } auto &animState = sceneAnimations[sceneName]; it->second->setActiveAnimation(animState, animationName, resetTime); return true; } bool SceneManager::setSceneAnimationLoop(const std::string &sceneName, bool loop) { auto it = loadedScenes.find(sceneName); if (it == loadedScenes.end() || !it->second) { return false; } auto &animState = sceneAnimations[sceneName]; animState.animationLoop = loop; return true; } bool SceneManager::setGLTFInstanceAnimation(const std::string &instanceName, int animationIndex, bool resetTime) { auto it = dynamicGLTFInstances.find(instanceName); if (it == dynamicGLTFInstances.end() || !it->second.scene) { return false; } LoadedGLTF::AnimationState &animState = it->second.animation; it->second.scene->setActiveAnimation(animState, animationIndex, resetTime); return true; } bool SceneManager::setGLTFInstanceAnimation(const std::string &instanceName, const std::string &animationName, bool resetTime) { auto it = dynamicGLTFInstances.find(instanceName); if (it == dynamicGLTFInstances.end() || !it->second.scene) { return false; } LoadedGLTF::AnimationState &animState = it->second.animation; it->second.scene->setActiveAnimation(animState, animationName, resetTime); return true; } bool SceneManager::setGLTFInstanceAnimationLoop(const std::string &instanceName, bool loop) { auto it = dynamicGLTFInstances.find(instanceName); if (it == dynamicGLTFInstances.end() || !it->second.scene) { return false; } it->second.animation.animationLoop = loop; return true; } void SceneManager::setSunlightDirection(const glm::vec3& dir) { glm::vec3 normalized = glm::normalize(dir); sceneData.sunlightDirection = glm::vec4(normalized, sceneData.sunlightDirection.w); } glm::vec3 SceneManager::getSunlightDirection() const { return glm::vec3(sceneData.sunlightDirection); } void SceneManager::setSunlightColor(const glm::vec3& color, float intensity) { sceneData.sunlightColor = glm::vec4(color, intensity); } glm::vec3 SceneManager::getSunlightColor() const { return glm::vec3(sceneData.sunlightColor); } float SceneManager::getSunlightIntensity() const { return sceneData.sunlightColor.w; }