#include "InteractiveObject.h" #include "render/Renderer.h" #include "render/TextureManager.h" #include "../TextModel.h" #include #include #include #include namespace ZL { extern const std::string textureUniformName; #ifdef EMSCRIPTEN using std::min; using std::max; #endif InteractiveObject InteractiveObject::createFromState(InteractiveObjectState st, Renderer& renderer, const std::string& zipPath) { InteractiveObject obj; obj.state = std::move(st); const LoadedGameObjectState& info = obj.state.objectInfo; obj.loadedObject.texture = renderer.textureManager.LoadFromPng(info.texturePath, zipPath); if (!info.textureDarkandsPath.empty()) obj.loadedObject.textureDarklands = renderer.textureManager.LoadFromPng(info.textureDarkandsPath, zipPath); if (info.meshPath.size() > 4 && info.meshPath.compare(info.meshPath.size() - 4, 4, ".bin") == 0) obj.loadedObject.mesh.data = LoadModelFromBinFile(info.meshPath, zipPath); else obj.loadedObject.mesh.data = LoadFromTextFile02(info.meshPath, zipPath); constexpr float kDeg2Rad = static_cast(M_PI) / 180.0f; Eigen::Quaternionf rot = Eigen::Quaternionf::Identity(); if (info.meshRotationX != 0.0f) rot = Eigen::Quaternionf(Eigen::AngleAxisf(info.meshRotationX * kDeg2Rad, Eigen::Vector3f::UnitX())) * rot; if (info.meshRotationY != 0.0f) rot = Eigen::Quaternionf(Eigen::AngleAxisf(info.meshRotationY * kDeg2Rad, Eigen::Vector3f::UnitY())) * rot; if (info.meshRotationZ != 0.0f) rot = Eigen::Quaternionf(Eigen::AngleAxisf(info.meshRotationZ * kDeg2Rad, Eigen::Vector3f::UnitZ())) * rot; obj.loadedObject.mesh.data.RotateByMatrix(rot.toRotationMatrix()); if (info.meshScale != 1.0f) obj.loadedObject.mesh.data.Scale(info.meshScale); obj.loadedObject.mesh.RefreshVBO(); // Center mesh at origin; adjust world position by the centering offset. if (!obj.loadedObject.mesh.data.PositionData.empty()) { Eigen::Vector3f meshMin = obj.loadedObject.mesh.data.PositionData[0]; Eigen::Vector3f meshMax = obj.loadedObject.mesh.data.PositionData[0]; for (const auto& v : obj.loadedObject.mesh.data.PositionData) { meshMin = meshMin.cwiseMin(v); meshMax = meshMax.cwiseMax(v); } const Eigen::Vector3f meshCenter = (meshMin + meshMax) * 0.5f; obj.loadedObject.mesh.data.Move(-meshCenter); obj.loadedObject.mesh.RefreshVBO(); obj.state.position = Eigen::Vector3f(obj.state.jsonPositionX, obj.state.jsonPositionY, obj.state.jsonPositionZ) + meshCenter; } else { obj.state.position = Eigen::Vector3f(obj.state.jsonPositionX, obj.state.jsonPositionY, obj.state.jsonPositionZ); } return obj; } void InteractiveObject::moveTo(const Eigen::Vector3f& target, float durationSec, std::function onComplete) { if (state.isAnimating) return; AnimTask task; task.type = AnimTask::Type::Move; task.startPos = state.position; task.startRotY = state.rotationY; task.startScale = state.scale; task.targetPos = target; task.targetRotY = state.rotationY; task.targetScale = state.scale; task.durationMs = durationSec * 1000.f; task.elapsedMs = 0.f; task.onComplete = std::move(onComplete); state.animTask = std::move(task); state.isAnimating = true; } void InteractiveObject::rotateTo(float targetRotY, float durationSec, std::function onComplete) { if (state.isAnimating) return; AnimTask task; task.type = AnimTask::Type::Rotate; task.startPos = state.position; task.startRotY = state.rotationY; task.startScale = state.scale; task.targetPos = state.position; task.targetRotY = targetRotY; task.targetScale = state.scale; task.durationMs = durationSec * 1000.f; task.elapsedMs = 0.f; task.onComplete = std::move(onComplete); state.animTask = std::move(task); state.isAnimating = true; } void InteractiveObject::scaleTo(float targetScale, float durationSec, std::function onComplete) { if (state.isAnimating) return; AnimTask task; task.type = AnimTask::Type::Scale; task.startPos = state.position; task.startRotY = state.rotationY; task.startScale = state.scale; task.targetPos = state.position; task.targetRotY = state.rotationY; task.targetScale = targetScale; task.durationMs = durationSec * 1000.f; task.elapsedMs = 0.f; task.onComplete = std::move(onComplete); state.animTask = std::move(task); state.isAnimating = true; } void InteractiveObject::fadeTo(float targetAlpha, float durationSec, std::function onComplete) { if (state.isAnimating) return; AnimTask task; task.type = AnimTask::Type::Fade; task.startPos = state.position; task.startRotY = state.rotationY; task.startScale = state.alpha; // reuse startScale to hold start alpha task.targetPos = state.position; task.targetRotY = state.rotationY; task.targetScale = targetAlpha; // reuse targetScale to hold target alpha task.durationMs = durationSec * 1000.f; task.elapsedMs = 0.f; task.onComplete = std::move(onComplete); state.animTask = std::move(task); state.isAnimating = true; } void InteractiveObject::update(int64_t deltaMs) { if (!state.isAnimating || !state.animTask) return; AnimTask& task = *state.animTask; task.elapsedMs += static_cast(deltaMs); const float t = min(task.elapsedMs / task.durationMs, 1.0f); switch (task.type) { case AnimTask::Type::Move: state.position = task.startPos + (task.targetPos - task.startPos) * t; break; case AnimTask::Type::Rotate: state.rotationY = task.startRotY + (task.targetRotY - task.startRotY) * t; break; case AnimTask::Type::Scale: state.scale = task.startScale + (task.targetScale - task.startScale) * t; break; case AnimTask::Type::Fade: state.alpha = task.startScale + (task.targetScale - task.startScale) * t; break; } if (t >= 1.0f) { state.position = task.targetPos; state.rotationY = task.targetRotY; if (task.type == AnimTask::Type::Fade) state.alpha = task.targetScale; else state.scale = task.targetScale; std::function cb = std::move(task.onComplete); state.animTask.reset(); state.isAnimating = false; if (cb) cb(); } } void InteractiveObject::drawDarklands(Renderer& renderer) const { if (!state.isActive || !loadedObject.textureDarklands) return; if (state.alpha < 0.999f) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } renderer.PushMatrix(); renderer.TranslateMatrix(state.position); if (state.rotationY != 0.f) { renderer.TranslateMatrix(state.pivot); renderer.RotateMatrix(Eigen::AngleAxisf(state.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix()); renderer.TranslateMatrix(-state.pivot); } if (state.scale != 1.f) renderer.ScaleMatrix(state.scale); renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1f("uAlpha", state.alpha); glBindTexture(GL_TEXTURE_2D, loadedObject.textureDarklands->getTexID()); renderer.DrawVertexRenderStruct(loadedObject.mesh); renderer.PopMatrix(); renderer.RenderUniform1f("uAlpha", 1.0f); } void InteractiveObject::draw(Renderer& renderer) const { if (!state.isActive || !loadedObject.texture) return; if (state.alpha < 0.999f) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } renderer.PushMatrix(); renderer.TranslateMatrix(state.position); if (state.rotationY != 0.f) { renderer.TranslateMatrix(state.pivot); renderer.RotateMatrix(Eigen::AngleAxisf(state.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix()); renderer.TranslateMatrix(-state.pivot); } if (state.scale != 1.f) renderer.ScaleMatrix(state.scale); renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1f("uAlpha", state.alpha); glBindTexture(GL_TEXTURE_2D, loadedObject.texture->getTexID()); renderer.DrawVertexRenderStruct(loadedObject.mesh); renderer.PopMatrix(); renderer.RenderUniform1f("uAlpha", 1.0f); } void InteractiveObjectState::save(nlohmann::json& out) const { out["positionX"] = position.x(); out["positionY"] = position.y(); out["positionZ"] = position.z(); out["rotationY"] = rotationY; out["scale"] = scale; out["alpha"] = alpha; out["isActive"] = isActive; // isAnimating and animTask are not saved: animations snap to their end // state on load (the final position/scale/alpha is already in the fields above). } void InteractiveObjectState::load(const nlohmann::json& in) { position.x() = in.value("positionX", position.x()); position.y() = in.value("positionY", position.y()); position.z() = in.value("positionZ", position.z()); rotationY = in.value("rotationY", rotationY); scale = in.value("scale", scale); alpha = in.value("alpha", alpha); isActive = in.value("isActive", isActive); // Cancel any in-flight animation; state was restored to final values above. isAnimating = false; animTask.reset(); } } // namespace ZL