Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74a2044441 | ||
|
|
134812dae3 | ||
|
|
29c413579d | ||
|
|
b6d0868e15 | ||
|
|
cad0481bc0 | ||
|
|
3403822a52 | ||
|
|
ea6beee593 | ||
|
|
d5a86c170e | ||
|
|
e1cef53158 | ||
|
|
82910c41e8 | ||
|
|
a744b332eb | ||
|
|
6c81bfd70d | ||
|
|
fae6614e70 | ||
|
|
e2a9c21e68 | ||
|
|
007d175314 | ||
|
|
ffe33e9967 | ||
|
|
9ff08651f0 | ||
|
|
c9d4eed043 | ||
|
|
21b33d7d7f | ||
|
|
a130850276 | ||
|
|
1d360369c9 |
@@ -32,3 +32,6 @@
|
|||||||
[submodule "Lib/ImGuizmo"]
|
[submodule "Lib/ImGuizmo"]
|
||||||
path = Lib/ImGuizmo
|
path = Lib/ImGuizmo
|
||||||
url = https://github.com/CedricGuillemet/ImGuizmo.git
|
url = https://github.com/CedricGuillemet/ImGuizmo.git
|
||||||
|
[submodule "Lib/yaml-cpp"]
|
||||||
|
path = Lib/yaml-cpp
|
||||||
|
url = https://github.com/jbeder/yaml-cpp.git
|
||||||
|
|||||||
+5
-2
@@ -1,9 +1,12 @@
|
|||||||
# App 层源文件:main + editor/(docking 编辑器,纯 App 代码)
|
# App 层源文件:main + editor/(docking 编辑器)+ scripts/(C++ 原生脚本)
|
||||||
file(GLOB EDITOR_SOURCES CONFIGURE_DEPENDS
|
file(GLOB EDITOR_SOURCES CONFIGURE_DEPENDS
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.h)
|
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.h)
|
||||||
|
|
||||||
add_executable(HelloVulkan main.cpp ${EDITOR_SOURCES})
|
file(GLOB SCRIPT_SOURCES CONFIGURE_DEPENDS
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/scripts/*.cpp)
|
||||||
|
|
||||||
|
add_executable(HelloVulkan main.cpp ${EDITOR_SOURCES} ${SCRIPT_SOURCES})
|
||||||
|
|
||||||
target_include_directories(HelloVulkan PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/editor)
|
target_include_directories(HelloVulkan PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/editor)
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,17 @@
|
|||||||
#include "ViewportPanel.h"
|
#include "ViewportPanel.h"
|
||||||
#include "StatsPanel.h"
|
#include "StatsPanel.h"
|
||||||
|
|
||||||
|
#include <Engine/Core/Log.h>
|
||||||
|
#include <Engine/Serialization/SceneSerializer.h>
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <imgui_internal.h> // DockBuilder*(内部 API)
|
#include <imgui_internal.h> // DockBuilder*(内部 API)
|
||||||
#include <ImGuizmo.h>
|
#include <ImGuizmo.h>
|
||||||
|
|
||||||
#include <GLFW/glfw3.h>
|
#include <GLFW/glfw3.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h)
|
using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h)
|
||||||
|
|
||||||
Editor::Editor() = default;
|
Editor::Editor() = default;
|
||||||
@@ -79,6 +84,7 @@ void Editor::Draw()
|
|||||||
if (m_ShowStats) m_Stats->Draw(m_Ctx);
|
if (m_ShowStats) m_Stats->Draw(m_Ctx);
|
||||||
|
|
||||||
DrawQuitModal();
|
DrawQuitModal();
|
||||||
|
DrawSceneModals();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里
|
// 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里
|
||||||
@@ -126,6 +132,15 @@ void Editor::DrawMenuBar()
|
|||||||
|
|
||||||
if (ImGui::BeginMenu("File"))
|
if (ImGui::BeginMenu("File"))
|
||||||
{
|
{
|
||||||
|
if (ImGui::MenuItem("Save Scene...", nullptr))
|
||||||
|
{
|
||||||
|
m_ShowSaveModal = true;
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem("Load Scene...", nullptr))
|
||||||
|
{
|
||||||
|
m_ShowLoadModal = true;
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
if (ImGui::MenuItem("Quit", "Alt+F4"))
|
if (ImGui::MenuItem("Quit", "Alt+F4"))
|
||||||
{
|
{
|
||||||
RequestQuit();
|
RequestQuit();
|
||||||
@@ -215,3 +230,141 @@ void Editor::DrawQuitModal()
|
|||||||
m_Ctx.app->Close();
|
m_Ctx.app->Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Editor::DrawSceneModals()
|
||||||
|
{
|
||||||
|
// 保存/加载共用一个路径输入(UE 同款:默认路径记住上次输入)
|
||||||
|
if (m_ShowSaveModal)
|
||||||
|
{
|
||||||
|
ImGui::OpenPopup("Save Scene");
|
||||||
|
m_ShowSaveModal = false;
|
||||||
|
}
|
||||||
|
if (m_ShowLoadModal)
|
||||||
|
{
|
||||||
|
ImGui::OpenPopup("Load Scene");
|
||||||
|
m_ShowLoadModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetNextWindowPos(
|
||||||
|
ImGui::GetMainViewport()->GetCenter(),
|
||||||
|
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||||||
|
|
||||||
|
if (ImGui::BeginPopupModal("Save Scene", nullptr,
|
||||||
|
ImGuiWindowFlags_AlwaysAutoResize))
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted("Scene file path (relative to working dir):");
|
||||||
|
ImGui::SetNextItemWidth(400.0f);
|
||||||
|
ImGui::InputText("##SavePath", m_ScenePathBuf, sizeof(m_ScenePathBuf));
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::Button("Save", ImVec2(120, 0)))
|
||||||
|
{
|
||||||
|
DoSaveScene();
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Cancel", ImVec2(120, 0)))
|
||||||
|
{
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui::BeginPopupModal("Load Scene", nullptr,
|
||||||
|
ImGuiWindowFlags_AlwaysAutoResize))
|
||||||
|
{
|
||||||
|
ImGui::TextUnformatted("Scene file path (relative to working dir):");
|
||||||
|
ImGui::SetNextItemWidth(400.0f);
|
||||||
|
ImGui::InputText("##LoadPath", m_ScenePathBuf, sizeof(m_ScenePathBuf));
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::Button("Load", ImVec2(120, 0)))
|
||||||
|
{
|
||||||
|
DoLoadScene();
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Cancel", ImVec2(120, 0)))
|
||||||
|
{
|
||||||
|
ImGui::CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
ImGui::EndPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Editor::DoSaveScene()
|
||||||
|
{
|
||||||
|
const std::string path = m_ScenePathBuf;
|
||||||
|
if (path.empty() || !m_Ctx.scene || !m_Ctx.assets)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// scenes/ 目录不存在时自动创建(空父目录/已存在均合法)
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(
|
||||||
|
std::filesystem::path(path).parent_path(), ec);
|
||||||
|
|
||||||
|
// 环境路径:引擎序列化器只认字符串,查表由编辑器侧负责
|
||||||
|
std::string envPath;
|
||||||
|
if (m_Ctx.getEnvIndex)
|
||||||
|
{
|
||||||
|
const int idx = m_Ctx.getEnvIndex();
|
||||||
|
if (idx >= 0 && idx < kEnvCount) envPath = kEnvPaths[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Serialization::SceneSerializer::Serialize(*m_Ctx.scene, *m_Ctx.assets,
|
||||||
|
path, envPath))
|
||||||
|
{
|
||||||
|
ENGINE_INFO("Editor: scene saved to '{}'", path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ENGINE_ERROR("Editor: failed to save scene to '{}'", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Editor::DoLoadScene()
|
||||||
|
{
|
||||||
|
const std::string path = m_ScenePathBuf;
|
||||||
|
if (path.empty() || !m_Ctx.scene || !m_Ctx.assets)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string envPath;
|
||||||
|
if (!Serialization::SceneSerializer::Deserialize(*m_Ctx.scene,
|
||||||
|
*m_Ctx.assets, path,
|
||||||
|
envPath))
|
||||||
|
{
|
||||||
|
ENGINE_ERROR("Editor: failed to load scene '{}'", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ENGINE_INFO("Editor: scene loaded from '{}'", path);
|
||||||
|
|
||||||
|
// 旧实体已销毁:清选中(连带清描边,见 SelectEntity)
|
||||||
|
SelectEntity(entt::null);
|
||||||
|
|
||||||
|
// 环境恢复:场景带环境且命中环境表才切换(镜像 StatsPanel 同步路径)
|
||||||
|
if (!envPath.empty() && m_Ctx.getEnvIndex && m_Ctx.setEnvIndex)
|
||||||
|
{
|
||||||
|
int found = -1;
|
||||||
|
for (int i = 0; i < kEnvCount; ++i)
|
||||||
|
{
|
||||||
|
if (envPath == kEnvPaths[i]) { found = i; break; }
|
||||||
|
}
|
||||||
|
if (found >= 0)
|
||||||
|
{
|
||||||
|
m_Ctx.setEnvIndex(found);
|
||||||
|
const auto env = m_Ctx.assets->LoadEnvironment(kEnvPaths[found]);
|
||||||
|
if (env.Valid())
|
||||||
|
{
|
||||||
|
m_Ctx.renderer->SetEnvironment(
|
||||||
|
m_Ctx.assets->GetEnvironment(env));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ENGINE_WARN("Editor: scene env '{}' not in env table, "
|
||||||
|
"keeping current", envPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ private:
|
|||||||
void DrawMenuBar(); // host 窗口内顶栏
|
void DrawMenuBar(); // host 窗口内顶栏
|
||||||
void DrawDockSpace(); // 全屏 host + dockspace + 默认布局
|
void DrawDockSpace(); // 全屏 host + dockspace + 默认布局
|
||||||
void DrawQuitModal(); // 退出确认模态框
|
void DrawQuitModal(); // 退出确认模态框
|
||||||
|
void DrawSceneModals(); // 保存/加载路径模态框(共用一个路径输入)
|
||||||
|
void DoSaveScene(); // 模态确认动作:目录创建 + 序列化落盘
|
||||||
|
void DoLoadScene(); // 模态确认动作:反序列化 + 选中清空 + 环境恢复 + 回调
|
||||||
|
|
||||||
EditorContext m_Ctx{};
|
EditorContext m_Ctx{};
|
||||||
|
|
||||||
@@ -98,6 +101,11 @@ private:
|
|||||||
// 退出确认链
|
// 退出确认链
|
||||||
bool m_ShowQuitConfirm = false;
|
bool m_ShowQuitConfirm = false;
|
||||||
bool m_QuitConfirmed = false;
|
bool m_QuitConfirmed = false;
|
||||||
|
|
||||||
|
// 场景保存/加载(File 菜单 → 路径模态框;无文件对话框库,InputText 手输)
|
||||||
|
bool m_ShowSaveModal = false;
|
||||||
|
bool m_ShowLoadModal = false;
|
||||||
|
char m_ScenePathBuf[256] = "scenes/sandbox.scene";
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //HELLOVULKAN_EDITOR_H
|
#endif //HELLOVULKAN_EDITOR_H
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <Engine/ECS/Scene.h>
|
#include <Engine/ECS/Scene.h>
|
||||||
#include <Engine/Assets/AssetManager.h>
|
#include <Engine/Assets/AssetManager.h>
|
||||||
#include <Engine/Renderer/Renderer.h>
|
#include <Engine/Renderer/Renderer.h>
|
||||||
|
#include <Engine/Renderer/EditorCamera.h>
|
||||||
#include <Engine/Renderer/SceneTypes.h> // kDisplayViewport / kDisplayDirect
|
#include <Engine/Renderer/SceneTypes.h> // kDisplayViewport / kDisplayDirect
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@@ -37,6 +38,9 @@ struct EditorContext {
|
|||||||
Engine::ECS::Scene *scene = nullptr;
|
Engine::ECS::Scene *scene = nullptr;
|
||||||
Engine::Assets::AssetManager *assets = nullptr;
|
Engine::Assets::AssetManager *assets = nullptr;
|
||||||
Engine::Application *app = nullptr;
|
Engine::Application *app = nullptr;
|
||||||
|
// 编辑器相机(Scene view 专用,归 Renderer 持有——与场景相机实体
|
||||||
|
// 无关;面板经此做 F 焦点/Gizmo 视图/对齐跳转)
|
||||||
|
Engine::EditorCamera *editorCamera = nullptr;
|
||||||
|
|
||||||
std::function<int()> getEnvIndex; // 当前环境表下标
|
std::function<int()> getEnvIndex; // 当前环境表下标
|
||||||
std::function<void(int)> setEnvIndex;
|
std::function<void(int)> setEnvIndex;
|
||||||
|
|||||||
+410
-38
@@ -8,11 +8,14 @@
|
|||||||
#include "InspectorPanel.h"
|
#include "InspectorPanel.h"
|
||||||
#include "Engine/ECS/Components.h"
|
#include "Engine/ECS/Components.h"
|
||||||
#include "Engine/Renderer/Model.h"
|
#include "Engine/Renderer/Model.h"
|
||||||
|
#include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面)
|
||||||
|
#include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names)
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -62,6 +65,44 @@ namespace
|
|||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 组件区块头(Unity 语义):折叠标题 + 右缘"启用"勾选与"删除"按钮。
|
||||||
|
// active 传 nullptr = 无启停(结构性组件);removable = 显示删除钮,
|
||||||
|
// 点击时置 *removeFlag,调用方在区块末尾统一移除(避免边画边删)
|
||||||
|
bool ComponentHeader(const char* label, bool* active, bool removable,
|
||||||
|
bool* removeFlag)
|
||||||
|
{
|
||||||
|
ImGui::PushID(label);
|
||||||
|
bool open;
|
||||||
|
if (active != nullptr)
|
||||||
|
{
|
||||||
|
open = ImGui::CollapsingHeader(
|
||||||
|
label, ImGuiTreeNodeFlags_DefaultOpen |
|
||||||
|
ImGuiTreeNodeFlags_AllowOverlap);
|
||||||
|
const float rowH = ImGui::GetItemRectSize().y;
|
||||||
|
float x = ImGui::GetWindowWidth() - 32.0f;
|
||||||
|
if (removable)
|
||||||
|
{
|
||||||
|
ImGui::SameLine(x);
|
||||||
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1.0f);
|
||||||
|
if (ImGui::Button("-", ImVec2(24.0f, rowH - 4.0f)))
|
||||||
|
{
|
||||||
|
*removeFlag = true;
|
||||||
|
}
|
||||||
|
x -= 30.0f;
|
||||||
|
}
|
||||||
|
ImGui::SameLine(x);
|
||||||
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1.0f);
|
||||||
|
ImGui::Checkbox("##active", active);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
open = ImGui::CollapsingHeader(label,
|
||||||
|
ImGuiTreeNodeFlags_DefaultOpen);
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
return open;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||||
@@ -83,46 +124,47 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
// ---- Add Component ----
|
// ---- Add Component(已挂载项置灰:Unity Add Component 语义)----
|
||||||
if (ImGui::Button("Add Component"))
|
if (ImGui::Button("Add Component"))
|
||||||
{
|
{
|
||||||
ImGui::OpenPopup("add_component");
|
ImGui::OpenPopup("add_component");
|
||||||
}
|
}
|
||||||
if (ImGui::BeginPopup("add_component"))
|
if (ImGui::BeginPopup("add_component"))
|
||||||
{
|
{
|
||||||
if (!reg.all_of<Engine::ECS::LightComponent>(selected) &&
|
auto menuItem = [&](auto typeTag, const char* label)
|
||||||
ImGui::MenuItem("Light"))
|
|
||||||
{
|
{
|
||||||
reg.emplace<Engine::ECS::LightComponent>(selected);
|
using T = std::remove_pointer_t<decltype(typeTag)>;
|
||||||
}
|
const bool has = reg.all_of<T>(selected);
|
||||||
if (!reg.all_of<Engine::ECS::CameraComponent>(selected) &&
|
if (has)
|
||||||
ImGui::MenuItem("Camera"))
|
{
|
||||||
{
|
ImGui::BeginDisabled(true);
|
||||||
reg.emplace<Engine::ECS::CameraComponent>(selected);
|
}
|
||||||
}
|
if (ImGui::MenuItem(label))
|
||||||
if (!reg.all_of<Engine::ECS::AnimationComponent>(selected) &&
|
{
|
||||||
ImGui::MenuItem("Animation"))
|
reg.emplace<T>(selected);
|
||||||
{
|
}
|
||||||
reg.emplace<Engine::ECS::AnimationComponent>(selected);
|
if (has)
|
||||||
}
|
{
|
||||||
if (!reg.all_of<Engine::ECS::NameComponent>(selected) &&
|
ImGui::EndDisabled();
|
||||||
ImGui::MenuItem("Name"))
|
}
|
||||||
{
|
};
|
||||||
reg.emplace<Engine::ECS::NameComponent>(selected);
|
menuItem(static_cast<Engine::ECS::LightComponent*>(nullptr), "Light");
|
||||||
}
|
menuItem(static_cast<Engine::ECS::CameraComponent*>(nullptr), "Camera");
|
||||||
if (!reg.all_of<Engine::ECS::RigidBodyComponent>(selected) &&
|
menuItem(static_cast<Engine::ECS::AnimationComponent*>(nullptr), "Animation");
|
||||||
ImGui::MenuItem("RigidBody"))
|
menuItem(static_cast<Engine::ECS::NameComponent*>(nullptr), "Name");
|
||||||
{
|
menuItem(static_cast<Engine::ECS::RigidBodyComponent*>(nullptr), "RigidBody");
|
||||||
reg.emplace<Engine::ECS::RigidBodyComponent>(selected);
|
menuItem(static_cast<Engine::ECS::JointComponent*>(nullptr), "Joint");
|
||||||
}
|
menuItem(static_cast<Engine::ECS::CharacterControllerComponent*>(nullptr),
|
||||||
|
"Character Controller");
|
||||||
|
menuItem(static_cast<Engine::ECS::ScriptComponent*>(nullptr), "Script");
|
||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
// ---- Transform ----
|
// ---- Transform(结构性组件:不可删、无启停)----
|
||||||
if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected))
|
if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected))
|
||||||
{
|
{
|
||||||
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen))
|
if (ComponentHeader("Transform", nullptr, false, nullptr))
|
||||||
{
|
{
|
||||||
glm::vec3 pos = t->GetPosition();
|
glm::vec3 pos = t->GetPosition();
|
||||||
if (DragVec3Row("position", pos, 0.05f))
|
if (DragVec3Row("position", pos, 0.05f))
|
||||||
@@ -145,9 +187,13 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
// ---- MeshRenderer ----
|
// ---- MeshRenderer ----
|
||||||
if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected))
|
if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected))
|
||||||
{
|
{
|
||||||
if (ImGui::CollapsingHeader("MeshRenderer",
|
bool removeMr = false;
|
||||||
ImGuiTreeNodeFlags_DefaultOpen))
|
if (ComponentHeader("MeshRenderer", &mr->active, true, &removeMr))
|
||||||
{
|
{
|
||||||
|
if (!mr->active)
|
||||||
|
{
|
||||||
|
ImGui::BeginDisabled(true); // 禁用 = 内容置灰(Unity 语义)
|
||||||
|
}
|
||||||
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
|
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
|
||||||
model != nullptr)
|
model != nullptr)
|
||||||
{
|
{
|
||||||
@@ -162,14 +208,27 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
||||||
"model: INVALID (dangling ref)");
|
"model: INVALID (dangling ref)");
|
||||||
}
|
}
|
||||||
|
if (!mr->active)
|
||||||
|
{
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removeMr)
|
||||||
|
{
|
||||||
|
reg.remove<Engine::ECS::MeshRendererComponent>(selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Light ----
|
// ---- Light ----
|
||||||
if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected))
|
if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected))
|
||||||
{
|
{
|
||||||
if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen))
|
bool removeLight = false;
|
||||||
|
if (ComponentHeader("Light", &light->active, true, &removeLight))
|
||||||
{
|
{
|
||||||
|
if (!light->active)
|
||||||
|
{
|
||||||
|
ImGui::BeginDisabled(true);
|
||||||
|
}
|
||||||
int type = static_cast<int>(light->type);
|
int type = static_cast<int>(light->type);
|
||||||
if (ImGui::Combo("type", &type, "Directional\0Point\0"))
|
if (ImGui::Combo("type", &type, "Directional\0Point\0"))
|
||||||
{
|
{
|
||||||
@@ -187,18 +246,31 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
|
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
|
||||||
ImGui::TextDisabled("(position = entity Transform)");
|
ImGui::TextDisabled("(position = entity Transform)");
|
||||||
}
|
}
|
||||||
|
if (!light->active)
|
||||||
|
{
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removeLight)
|
||||||
|
{
|
||||||
|
reg.remove<Engine::ECS::LightComponent>(selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Camera ----
|
// ---- Camera ----
|
||||||
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
|
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
|
||||||
{
|
{
|
||||||
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen))
|
bool removeCam = false;
|
||||||
|
if (ComponentHeader("Camera", &cam->active, true, &removeCam))
|
||||||
{
|
{
|
||||||
|
if (!cam->active)
|
||||||
|
{
|
||||||
|
ImGui::BeginDisabled(true);
|
||||||
|
}
|
||||||
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
|
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
|
||||||
{
|
{
|
||||||
// primary 互斥:启用本相机时清掉其余实体的 primary
|
// primary 互斥:启用本相机时清掉其余实体的 primary
|
||||||
//(引擎取第一个 primary 作为渲染视角)
|
//(引擎取第一个 primary 作为游戏形态渲染视角)
|
||||||
reg.view<Engine::ECS::CameraComponent>().each(
|
reg.view<Engine::ECS::CameraComponent>().each(
|
||||||
[&](entt::entity e, Engine::ECS::CameraComponent& c)
|
[&](entt::entity e, Engine::ECS::CameraComponent& c)
|
||||||
{
|
{
|
||||||
@@ -208,6 +280,26 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 对齐跳转:把本实体位姿复制给编辑器相机,Scene view 立即
|
||||||
|
// 从该位置观看(只取位姿,不接管渲染——渲染源分派在
|
||||||
|
// SceneRenderer,编辑器形态恒用编辑器相机)
|
||||||
|
if (ImGui::Button("Align Viewport to Camera") && ctx.editorCamera)
|
||||||
|
{
|
||||||
|
if (auto* tr = reg.try_get<Engine::ECS::TransformComponent>(
|
||||||
|
selected))
|
||||||
|
{
|
||||||
|
ctx.editorCamera->AlignTo(*tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!cam->active)
|
||||||
|
{
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removeCam)
|
||||||
|
{
|
||||||
|
reg.remove<Engine::ECS::CameraComponent>(selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,15 +307,29 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
// type/collider/mass 变更 → PhysicsWorld 重建 body;其余参数即时生效
|
// type/collider/mass 变更 → PhysicsWorld 重建 body;其余参数即时生效
|
||||||
if (auto* rb = reg.try_get<Engine::ECS::RigidBodyComponent>(selected))
|
if (auto* rb = reg.try_get<Engine::ECS::RigidBodyComponent>(selected))
|
||||||
{
|
{
|
||||||
if (ImGui::CollapsingHeader("RigidBody", ImGuiTreeNodeFlags_DefaultOpen))
|
bool removeRb = false;
|
||||||
|
if (ComponentHeader("RigidBody", &rb->active, true, &removeRb))
|
||||||
{
|
{
|
||||||
|
if (!rb->active)
|
||||||
|
{
|
||||||
|
ImGui::BeginDisabled(true);
|
||||||
|
}
|
||||||
|
// 冲突提示:同实体挂 CharacterController 会同时建 body 与
|
||||||
|
// CharacterVirtual,两个"运动主体"互相打架
|
||||||
|
if (reg.all_of<Engine::ECS::CharacterControllerComponent>(selected))
|
||||||
|
{
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||||
|
"! conflicts with Character Controller "
|
||||||
|
"(disable or remove one)");
|
||||||
|
}
|
||||||
int type = static_cast<int>(rb->type);
|
int type = static_cast<int>(rb->type);
|
||||||
if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0"))
|
if (ImGui::Combo("type", &type, "Static\0Dynamic\0Kinematic\0"))
|
||||||
{
|
{
|
||||||
rb->type = static_cast<Engine::ECS::RigidBodyComponent::BodyType>(type);
|
rb->type = static_cast<Engine::ECS::RigidBodyComponent::BodyType>(type);
|
||||||
}
|
}
|
||||||
int collider = static_cast<int>(rb->collider);
|
int collider = static_cast<int>(rb->collider);
|
||||||
if (ImGui::Combo("collider", &collider, "Box\0Sphere\0Mesh\0"))
|
if (ImGui::Combo("collider", &collider,
|
||||||
|
"Box\0Sphere\0Mesh\0Capsule\0Cylinder\0Compound\0"))
|
||||||
{
|
{
|
||||||
rb->collider = static_cast<Engine::ECS::RigidBodyComponent::ColliderType>(collider);
|
rb->collider = static_cast<Engine::ECS::RigidBodyComponent::ColliderType>(collider);
|
||||||
}
|
}
|
||||||
@@ -235,6 +341,61 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
{
|
{
|
||||||
ImGui::DragFloat("radius", &rb->sphereRadius, 0.05f, 0.01f, 500.0f);
|
ImGui::DragFloat("radius", &rb->sphereRadius, 0.05f, 0.01f, 500.0f);
|
||||||
}
|
}
|
||||||
|
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Capsule)
|
||||||
|
{
|
||||||
|
// 半高 = 圆柱段半高(不含半球端盖),总高 = 2*(halfHeight + radius)
|
||||||
|
ImGui::DragFloat("radius", &rb->capsuleRadius, 0.05f, 0.01f, 500.0f);
|
||||||
|
ImGui::DragFloat("halfHeight", &rb->capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
|
||||||
|
}
|
||||||
|
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Cylinder)
|
||||||
|
{
|
||||||
|
ImGui::DragFloat("radius", &rb->cylinderRadius, 0.05f, 0.01f, 500.0f);
|
||||||
|
ImGui::DragFloat("halfHeight", &rb->cylinderHalfHeight, 0.05f, 0.01f, 500.0f);
|
||||||
|
}
|
||||||
|
else if (rb->collider == Engine::ECS::RigidBodyComponent::ColliderType::Compound)
|
||||||
|
{
|
||||||
|
// 子形状列表:尺寸/位置变更 → PhysicsWorld 重建 body(既有模式)
|
||||||
|
ImGui::Text("children: %d", static_cast<int>(rb->compound.size()));
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("+ add"))
|
||||||
|
{
|
||||||
|
rb->compound.emplace_back();
|
||||||
|
}
|
||||||
|
int remove = -1;
|
||||||
|
for (int i = 0; i < static_cast<int>(rb->compound.size()); ++i)
|
||||||
|
{
|
||||||
|
auto& c = rb->compound[i];
|
||||||
|
ImGui::PushID(i);
|
||||||
|
ImGui::Separator();
|
||||||
|
int sh = static_cast<int>(c.shape);
|
||||||
|
if (ImGui::Combo("shape", &sh, "Box\0Sphere\0Capsule\0"))
|
||||||
|
{
|
||||||
|
c.shape = static_cast<Engine::ECS::RigidBodyComponent::CompoundChild::Shape>(sh);
|
||||||
|
}
|
||||||
|
if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Box)
|
||||||
|
{
|
||||||
|
ImGui::DragFloat3("halfExtents", &c.boxHalfExtents.x, 0.05f, 0.01f, 500.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui::DragFloat("radius", &c.radius, 0.05f, 0.01f, 500.0f);
|
||||||
|
if (c.shape == Engine::ECS::RigidBodyComponent::CompoundChild::Shape::Capsule)
|
||||||
|
{
|
||||||
|
ImGui::DragFloat("halfHeight", &c.capsuleHalfHeight, 0.05f, 0.0f, 500.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui::DragFloat3("offset", &c.position.x, 0.05f);
|
||||||
|
if (ImGui::Button("remove"))
|
||||||
|
{
|
||||||
|
remove = i;
|
||||||
|
}
|
||||||
|
ImGui::PopID();
|
||||||
|
}
|
||||||
|
if (remove >= 0)
|
||||||
|
{
|
||||||
|
rb->compound.erase(rb->compound.begin() + remove);
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Mesh:跟随 MeshRenderer 视觉网格(Static=精确网格,
|
// Mesh:跟随 MeshRenderer 视觉网格(Static=精确网格,
|
||||||
@@ -245,27 +406,230 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
ImGui::SliderFloat("gravityScale", &rb->gravityScale, -1.0f, 5.0f);
|
ImGui::SliderFloat("gravityScale", &rb->gravityScale, -1.0f, 5.0f);
|
||||||
ImGui::SliderFloat("friction", &rb->friction, 0.0f, 2.0f);
|
ImGui::SliderFloat("friction", &rb->friction, 0.0f, 2.0f);
|
||||||
ImGui::SliderFloat("restitution", &rb->restitution, 0.0f, 1.0f);
|
ImGui::SliderFloat("restitution", &rb->restitution, 0.0f, 1.0f);
|
||||||
ImGui::Checkbox("isSensor", &rb->isSensor);
|
ImGui::Checkbox("isTrigger", &rb->isTrigger);
|
||||||
if (rb->bodyValid &&
|
if (rb->bodyValid &&
|
||||||
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic)
|
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic)
|
||||||
{
|
{
|
||||||
ImGui::Text("vel: %.2f, %.2f, %.2f m/s",
|
ImGui::Text("vel: %.2f, %.2f, %.2f m/s",
|
||||||
rb->velocity.x, rb->velocity.y, rb->velocity.z);
|
rb->velocity.x, rb->velocity.y, rb->velocity.z);
|
||||||
|
|
||||||
|
// 驱动调试(冲量瞬态不序列化):直接推一把看响应
|
||||||
|
static glm::vec3 s_debugImpulse{0.0f, 5.0f, 0.0f};
|
||||||
|
ImGui::DragFloat3("impulse", &s_debugImpulse.x, 0.1f);
|
||||||
|
if (ImGui::Button("Apply Impulse"))
|
||||||
|
{
|
||||||
|
ctx.app->GetPhysics().AddImpulse(
|
||||||
|
static_cast<uint32_t>(entt::to_integral(selected)),
|
||||||
|
s_debugImpulse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (!rb->active)
|
||||||
|
{
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removeRb)
|
||||||
|
{
|
||||||
|
reg.remove<Engine::ECS::RigidBodyComponent>(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Joint(约束:两端 body 就绪后由 PhysicsWorld 对账创建)----
|
||||||
|
if (auto* jc = reg.try_get<Engine::ECS::JointComponent>(selected))
|
||||||
|
{
|
||||||
|
bool removeJc = false;
|
||||||
|
if (ComponentHeader("Joint", &jc->active, true, &removeJc))
|
||||||
|
{
|
||||||
|
if (!jc->active)
|
||||||
|
{
|
||||||
|
ImGui::BeginDisabled(true);
|
||||||
|
}
|
||||||
|
// 依赖提示:约束挂在两端刚体上,本体无 RigidBody 无法创建
|
||||||
|
if (!reg.all_of<Engine::ECS::RigidBodyComponent>(selected))
|
||||||
|
{
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||||
|
"! requires a RigidBody on this entity");
|
||||||
|
}
|
||||||
|
int type = static_cast<int>(jc->type);
|
||||||
|
if (ImGui::Combo("type", &type, "Fixed\0Hinge\0Ball\0"))
|
||||||
|
{
|
||||||
|
jc->type = static_cast<Engine::ECS::JointComponent::Type>(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// target 实体下拉:实体名列表,选中态按 UUID 匹配(变更写回)
|
||||||
|
std::string items;
|
||||||
|
std::vector<Engine::UUID> uuids;
|
||||||
|
int current = -1;
|
||||||
|
reg.view<Engine::ECS::IdComponent, Engine::ECS::NameComponent>().each(
|
||||||
|
[&](entt::entity e, const Engine::ECS::IdComponent& id,
|
||||||
|
const Engine::ECS::NameComponent& n)
|
||||||
|
{
|
||||||
|
if (e == selected)
|
||||||
|
{
|
||||||
|
return; // 自连无意义
|
||||||
|
}
|
||||||
|
if (id.uuid == jc->target)
|
||||||
|
{
|
||||||
|
current = static_cast<int>(uuids.size());
|
||||||
|
}
|
||||||
|
uuids.push_back(id.uuid);
|
||||||
|
items.append(n.name);
|
||||||
|
items.push_back('\0');
|
||||||
|
});
|
||||||
|
items.push_back('\0'); // ImGui Combo 双 \0 结尾
|
||||||
|
if (ImGui::Combo("target", ¤t, items.c_str()) && current >= 0)
|
||||||
|
{
|
||||||
|
jc->target = uuids[static_cast<size_t>(current)];
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::DragFloat3("anchor A", &jc->anchorA.x, 0.05f);
|
||||||
|
ImGui::DragFloat3("anchor B", &jc->anchorB.x, 0.05f);
|
||||||
|
if (jc->type == Engine::ECS::JointComponent::Type::Hinge)
|
||||||
|
{
|
||||||
|
ImGui::DragFloat3("hinge axis", &jc->hingeAxis.x, 0.05f);
|
||||||
|
ImGui::Checkbox("enable limit", &jc->enableLimit);
|
||||||
|
if (jc->enableLimit)
|
||||||
|
{
|
||||||
|
ImGui::DragFloat("min deg", &jc->limitMin, 1.0f, -180.0f, 0.0f);
|
||||||
|
ImGui::DragFloat("max deg", &jc->limitMax, 1.0f, 0.0f, 180.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!jc->active)
|
||||||
|
{
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removeJc)
|
||||||
|
{
|
||||||
|
reg.remove<Engine::ECS::JointComponent>(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CharacterController(参数编辑;状态在 PhysicsWorld 运行时)----
|
||||||
|
if (auto* cc = reg.try_get<Engine::ECS::CharacterControllerComponent>(selected))
|
||||||
|
{
|
||||||
|
bool removeCc = false;
|
||||||
|
if (ComponentHeader("Character Controller", &cc->active, true, &removeCc))
|
||||||
|
{
|
||||||
|
if (!cc->active)
|
||||||
|
{
|
||||||
|
ImGui::BeginDisabled(true);
|
||||||
|
}
|
||||||
|
// 冲突提示:同实体挂 RigidBody 会同时建 CharacterVirtual 与 body
|
||||||
|
if (reg.all_of<Engine::ECS::RigidBodyComponent>(selected))
|
||||||
|
{
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||||
|
"! conflicts with RigidBody "
|
||||||
|
"(disable or remove one)");
|
||||||
|
}
|
||||||
|
// 实体原点 = 脚底(胶囊底端),总高 = 2*(halfHeight + radius)
|
||||||
|
ImGui::DragFloat("radius", &cc->radius, 0.02f, 0.05f, 5.0f);
|
||||||
|
ImGui::DragFloat("halfHeight", &cc->capsuleHalfHeight, 0.02f, 0.0f, 5.0f);
|
||||||
|
ImGui::DragFloat("mass", &cc->mass, 1.0f, 1.0f, 500.0f, "%.0f");
|
||||||
|
ImGui::DragFloat("max slope deg", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f);
|
||||||
|
ImGui::DragFloat("step height", &cc->stepHeight, 0.02f, 0.0f, 2.0f);
|
||||||
|
|
||||||
|
// 遥测(只读):运行时状态在 PhysicsWorld 的 CharacterController
|
||||||
|
if (auto& phy = ctx.app->GetPhysics(); phy.IsEnabled())
|
||||||
|
{
|
||||||
|
const uint32_t bits =
|
||||||
|
static_cast<uint32_t>(entt::to_integral(selected));
|
||||||
|
ImGui::Text("grounded: %s",
|
||||||
|
phy.IsCharacterGrounded(bits) ? "yes" : "no");
|
||||||
|
const glm::vec3 v = phy.GetCharacterVelocity(bits);
|
||||||
|
ImGui::Text("vel: %.2f, %.2f, %.2f m/s", v.x, v.y, v.z);
|
||||||
|
|
||||||
|
// 跳跃调试按钮
|
||||||
|
if (ImGui::Button("Jump"))
|
||||||
|
{
|
||||||
|
phy.Jump(bits, 5.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!cc->active)
|
||||||
|
{
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removeCc)
|
||||||
|
{
|
||||||
|
reg.remove<Engine::ECS::CharacterControllerComponent>(selected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Script(按名解析注册表脚本;实例生命周期在 ScriptRuntime)----
|
||||||
|
if (auto* sc = reg.try_get<Engine::ECS::ScriptComponent>(selected))
|
||||||
|
{
|
||||||
|
bool removeSc = false;
|
||||||
|
if (ComponentHeader("Script", &sc->active, true, &removeSc))
|
||||||
|
{
|
||||||
|
if (!sc->active)
|
||||||
|
{
|
||||||
|
ImGui::BeginDisabled(true);
|
||||||
|
}
|
||||||
|
// 下拉列出全部已注册脚本(注册序);"<none>" = 清空(对账自动销毁实例)
|
||||||
|
const auto& names = Engine::Scripting::ScriptRegistry::Names();
|
||||||
|
std::vector<const char*> items;
|
||||||
|
items.push_back("<none>");
|
||||||
|
for (const auto& n : names)
|
||||||
|
{
|
||||||
|
items.push_back(n.c_str());
|
||||||
|
}
|
||||||
|
int cur = 0;
|
||||||
|
for (size_t i = 0; i < names.size(); ++i)
|
||||||
|
{
|
||||||
|
if (names[i] == sc->scriptName)
|
||||||
|
{
|
||||||
|
cur = static_cast<int>(i) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ImGui::Combo("script", &cur, items.data(),
|
||||||
|
static_cast<int>(items.size())))
|
||||||
|
{
|
||||||
|
sc->scriptName = (cur <= 0) ? "" : names[static_cast<size_t>(cur) - 1];
|
||||||
|
}
|
||||||
|
if (sc->scriptName.empty())
|
||||||
|
{
|
||||||
|
ImGui::TextDisabled("no script assigned");
|
||||||
|
}
|
||||||
|
// 已填名但注册表查无此类:场景加载早于脚本注册/改名都会走到这
|
||||||
|
else if (cur == 0)
|
||||||
|
{
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||||
|
"! script not registered: %s",
|
||||||
|
sc->scriptName.c_str());
|
||||||
|
}
|
||||||
|
if (!sc->active)
|
||||||
|
{
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removeSc)
|
||||||
|
{
|
||||||
|
reg.remove<Engine::ECS::ScriptComponent>(selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Animation ----
|
// ---- Animation ----
|
||||||
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
|
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
|
||||||
{
|
{
|
||||||
if (ImGui::CollapsingHeader("Animation",
|
bool removeAnim = false;
|
||||||
ImGuiTreeNodeFlags_DefaultOpen))
|
if (ComponentHeader("Animation", &anim->active, true, &removeAnim))
|
||||||
{
|
{
|
||||||
|
if (!anim->active)
|
||||||
|
{
|
||||||
|
ImGui::BeginDisabled(true);
|
||||||
|
}
|
||||||
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
|
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
|
||||||
const Engine::Model* model =
|
const Engine::Model* model =
|
||||||
mr ? ctx.assets->GetModel(mr->model) : nullptr;
|
mr ? ctx.assets->GetModel(mr->model) : nullptr;
|
||||||
|
|
||||||
if (model != nullptr && model->HasAnimations())
|
// 依赖提示:动画作用在 MeshRenderer 的模型骨骼上
|
||||||
|
if (mr == nullptr)
|
||||||
|
{
|
||||||
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||||
|
"! requires a MeshRenderer component");
|
||||||
|
}
|
||||||
|
else if (model != nullptr && model->HasAnimations())
|
||||||
{
|
{
|
||||||
const auto& clips = model->GetClips();
|
const auto& clips = model->GetClips();
|
||||||
std::vector<const char*> clipNames;
|
std::vector<const char*> clipNames;
|
||||||
@@ -296,6 +660,14 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
|||||||
{
|
{
|
||||||
ImGui::TextDisabled("(no animation clips on model)");
|
ImGui::TextDisabled("(no animation clips on model)");
|
||||||
}
|
}
|
||||||
|
if (!anim->active)
|
||||||
|
{
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removeAnim)
|
||||||
|
{
|
||||||
|
reg.remove<Engine::ECS::AnimationComponent>(selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,18 +19,27 @@ void StatsPanel::Draw(const EditorContext& ctx)
|
|||||||
auto& renderer = *ctx.renderer;
|
auto& renderer = *ctx.renderer;
|
||||||
auto& app = *ctx.app;
|
auto& app = *ctx.app;
|
||||||
|
|
||||||
// ---- Camera:主相机位姿(只读遥测;位姿编辑走 Inspector)----
|
// ---- Camera:视图相机位姿(只读遥测;跟随渲染源分派)----
|
||||||
{
|
{
|
||||||
Engine::ECS::TransformComponent* camT = nullptr;
|
if (ctx.renderer->GetDisplayMode() == kDisplayViewport && ctx.editorCamera)
|
||||||
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr)
|
|
||||||
{
|
{
|
||||||
const glm::vec3& pos = camT->GetPosition();
|
const glm::vec3& pos = ctx.editorCamera->GetTransform().GetPosition();
|
||||||
ImGui::Text("Camera pos");
|
ImGui::Text("Editor camera pos");
|
||||||
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
|
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImGui::TextDisabled("(no primary camera)");
|
Engine::ECS::TransformComponent* camT = nullptr;
|
||||||
|
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr)
|
||||||
|
{
|
||||||
|
const glm::vec3& pos = camT->GetPosition();
|
||||||
|
ImGui::Text("Game camera pos");
|
||||||
|
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui::TextDisabled("(no primary camera)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-31
@@ -40,6 +40,20 @@ namespace
|
|||||||
constexpr float kSnapRotateDeg = 15.0f; // 度
|
constexpr float kSnapRotateDeg = 15.0f; // 度
|
||||||
constexpr float kSnapScale = 0.1f; // 缩放步进
|
constexpr float kSnapScale = 0.1f; // 缩放步进
|
||||||
|
|
||||||
|
// 平移拖拽灵敏度下限:sinθ(轴与"相机→物体"连线夹角)。低于该值
|
||||||
|
// 时拖拽投影系数 <0.25 且激活平面与鼠标射线近共面——视觉上
|
||||||
|
// "看得见、抓了没反应"。≈14.5°,略宽于库默认可见角(≈11.5°),
|
||||||
|
// 只藏真正病态的轴,不动可用轴
|
||||||
|
constexpr float kMoveMinSin = 0.25f;
|
||||||
|
|
||||||
|
// 缩放退化轴裁剪(防误拖动)阈值。库内缩放 hover/拖拽依赖
|
||||||
|
// "法线=轴"平面求交,两种病态几何:
|
||||||
|
// - 轴⊥"相机→物体"连线(正对视角无横偏):交点分子 numer =
|
||||||
|
// -dist·dot(camToObj, 轴) ≡ 0 → 杆抓不到、数值随机猛跳
|
||||||
|
// - 轴∥视线(轴指向相机):屏幕零位移 → 拖了数值不变
|
||||||
|
constexpr float kScaleMinDot = 0.1f; // ≈84.3°
|
||||||
|
constexpr float kScaleMaxFwdDot = 0.98f; // ≈11.5°,与库藏杆角一致
|
||||||
|
|
||||||
// Vulkan proj → ImGuizmo 期望的 OpenGL 形式(Y-up、Z∈[-1,1]):
|
// Vulkan proj → ImGuizmo 期望的 OpenGL 形式(Y-up、Z∈[-1,1]):
|
||||||
// Y 行翻回(抵消 Vulkan clip Y 向下翻转);z 行 = 2*z - w 即
|
// Y 行翻回(抵消 Vulkan clip Y 向下翻转);z 行 = 2*z - w 即
|
||||||
// 深度 [0,1] → [-1,1]。glm 列主序 m[col][row],逐列改 row1/row2
|
// 深度 [0,1] → [-1,1]。glm 列主序 m[col][row],逐列改 row1/row2
|
||||||
@@ -52,6 +66,22 @@ namespace
|
|||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 视图相机分派(必须与 SceneRenderer::BuildFrame 渲染源同一规则,
|
||||||
|
// 否则 Gizmo 杆与画面错位):编辑器形态 = EditorCamera(Scene view
|
||||||
|
// 专用,与相机实体无关);游戏形态 = primary 相机实体。
|
||||||
|
// 返回 nullptr = 游戏形态且无 primary 相机(本帧无 Gizmo)
|
||||||
|
Engine::Camera* ViewCamera(const EditorContext& ctx,
|
||||||
|
ECS::TransformComponent*& outT)
|
||||||
|
{
|
||||||
|
if (ctx.renderer->GetDisplayMode() == kDisplayViewport)
|
||||||
|
{
|
||||||
|
outT = &ctx.editorCamera->GetTransform();
|
||||||
|
return &ctx.editorCamera->GetController();
|
||||||
|
}
|
||||||
|
auto* cc = ctx.scene->GetPrimaryCamera(&outT);
|
||||||
|
return cc ? &cc->camera : nullptr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||||
@@ -111,48 +141,52 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
|||||||
else size.y = avail.x / aspect;
|
else size.y = avail.x / aspect;
|
||||||
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
|
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
|
||||||
|
|
||||||
// ---- W/E/R 切换 Gizmo 模式(UE 语义:W 平移 / E 旋转 /
|
// ---- 工具热键(UE 语义):W/E/R 切 Gizmo 模式、T 切
|
||||||
// R 缩放),T 切换 local/world 操纵空间。仅 Viewport 持焦时
|
// local/world、F 焦点选中实体。仅 Viewport 持焦时生效——
|
||||||
// 生效——输入框打字(WantTextInput)由 BlockingKeyboard 拦
|
// 输入框打字(WantTextInput)由 BlockingKeyboard 拦游戏侧,
|
||||||
// 游戏侧,但 ImGui 自身仍收键,故必须限定持焦面板,避免在
|
// 但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按住 = 相机
|
||||||
// Inspector 拖数值时误切 ----
|
// 飞行模式(键盘归相机),工具热键全部抑制——W 前进与
|
||||||
if (out.focused && !ImGui::GetIO().WantTextInput) {
|
// W 平移 Gizmo 的键位冲突就此解耦 ----
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_W)) gizmo.op = ImGuizmo::TRANSLATE;
|
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE;
|
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE;
|
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_T)) gizmo.localSpace = !gizmo.localSpace;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
|
|
||||||
// 选中实体 + 有 Transform 才出杆;矩阵分解直接写回
|
|
||||||
// TransformComponent(与 Inspector 同源,下一帧数值自动跟随)
|
|
||||||
auto ® = ctx.scene->GetRegistry();
|
auto ® = ctx.scene->GetRegistry();
|
||||||
ECS::TransformComponent *tr =
|
ECS::TransformComponent *tr =
|
||||||
selected != entt::null
|
selected != entt::null
|
||||||
? reg.try_get<ECS::TransformComponent>(selected)
|
? reg.try_get<ECS::TransformComponent>(selected)
|
||||||
: nullptr;
|
: nullptr;
|
||||||
|
if (out.focused && !ImGui::GetIO().WantTextInput &&
|
||||||
|
!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_W)) gizmo.op = ImGuizmo::TRANSLATE;
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE;
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE;
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_T)) gizmo.localSpace = !gizmo.localSpace;
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_F) && tr != nullptr) {
|
||||||
|
// F = 焦点选中实体(frame-selection):编辑器相机
|
||||||
|
// 环绕 pivot 设为选中实体位置,沿视线平移对准(环
|
||||||
|
// 绕距离沿用当前值,滚轮可调)
|
||||||
|
ctx.editorCamera->FocusOn(tr->GetPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
|
||||||
|
// 选中实体 + 有 Transform 才出杆;矩阵分解直接写回
|
||||||
|
// TransformComponent(与 Inspector 同源,下一帧数值自动跟随)
|
||||||
if (tr != nullptr) {
|
if (tr != nullptr) {
|
||||||
// 相机矩阵:primary 相机 view + Vulkan proj(转 GL 形式),
|
// 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏
|
||||||
// 参数与渲染侧一致(aspect = 渲染分辨率、near/far 0.1/100)
|
// 形态 = primary 相机实体;与 SceneRenderer::BuildFrame
|
||||||
|
// 同一分派),参数与渲染侧一致(aspect = 渲染分辨率、
|
||||||
|
// near/far 0.1/100)
|
||||||
ECS::TransformComponent *camT = nullptr;
|
ECS::TransformComponent *camT = nullptr;
|
||||||
if (const auto *camComp = ctx.scene->GetPrimaryCamera(&camT);
|
if (Engine::Camera *cam = ViewCamera(ctx, camT);
|
||||||
camComp != nullptr && camT != nullptr) {
|
cam != nullptr && camT != nullptr) {
|
||||||
const glm::mat4 view = camComp->camera.GetView(*camT);
|
const glm::mat4 view = cam->GetView(*camT);
|
||||||
const glm::mat4 projGL = ProjVulkanToGL(
|
const glm::mat4 projGL = ProjVulkanToGL(
|
||||||
camComp->camera.GetProjection(aspect, 0.1f, 100.0f));
|
cam->GetProjection(aspect, 0.1f, 100.0f));
|
||||||
|
|
||||||
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
|
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
|
||||||
// 杆画在 Image 区域内(等比适配后的 item rect)
|
// 杆画在 Image 区域内(等比适配后的 item rect)
|
||||||
const ImVec2 mn = ImGui::GetItemRectMin();
|
const ImVec2 mn = ImGui::GetItemRectMin();
|
||||||
ImGuizmo::SetRect(mn.x, mn.y, size.x, size.y);
|
ImGuizmo::SetRect(mn.x, mn.y, size.x, size.y);
|
||||||
|
|
||||||
// 轴可见度阈值:库内轴恒定屏幕尺寸(mGizmoSizeClipSpace
|
// 不调 SetPlaneLimit/SetAxisLimit——可见度保持库默认
|
||||||
// =0.1),可见判定 = sinθ(轴与视线夹角) > 阈值/0.1。
|
|
||||||
// 默认 0.02 ⇒ θ>11.5° 就可见——"有高度差"的正对视角
|
|
||||||
// (θ≈15°)轴"看得见但几何上不可操作"(⊥视线轴悬停
|
|
||||||
// 失效、∥视线轴拖拽无一阶位移)。取 0.034 ⇒ θ<20° 的
|
|
||||||
// 轴隐藏(UE 正对轴隐藏语义),≥20° 正常可拖
|
|
||||||
ImGuizmo::SetPlaneLimit(0.034f);
|
|
||||||
|
|
||||||
glm::mat4 m = tr->World();
|
glm::mat4 m = tr->World();
|
||||||
// 操作空间:local(模型轴,旋转/缩放随物体朝向直观)/
|
// 操作空间:local(模型轴,旋转/缩放随物体朝向直观)/
|
||||||
@@ -160,11 +194,53 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
|||||||
// decompose 写回链路不变
|
// decompose 写回链路不变
|
||||||
const ImGuizmo::MODE mode =
|
const ImGuizmo::MODE mode =
|
||||||
gizmo.localSpace ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
|
gizmo.localSpace ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
|
||||||
|
const glm::mat3 rotM(m);
|
||||||
|
|
||||||
// Ctrl 按住 → 吸附(UE Grid Snap 语义)。库的约定
|
// 退化轴裁剪(防误拖动):被裁轴不可见、不可激活,
|
||||||
|
// 转个视角即可用。三操作病态几何不同,按 op 分派:
|
||||||
|
// - 平移:拖拽灵敏度 = sinθ(轴与相机连线夹角),θ→0
|
||||||
|
// 时鼠标位移在轴上投影→0("数值不变")
|
||||||
|
// - 缩放:库内 hover/拖拽依赖"法线=轴"平面求交——
|
||||||
|
// 轴⊥相机连线(正对无横偏)时交点分子 numer≡0
|
||||||
|
// (抓不到/数值乱跳);轴∥视线时屏幕零位移(数值
|
||||||
|
// 不变)。两判据并集裁剪
|
||||||
|
// - 旋转:屏幕空间角度数学,无退化,不裁
|
||||||
|
const bool isTranslate = (gizmo.op
|
||||||
|
& int(ImGuizmo::TRANSLATE)) == int(ImGuizmo::TRANSLATE);
|
||||||
|
const bool isScale = (gizmo.op
|
||||||
|
& int(ImGuizmo::SCALE)) == int(ImGuizmo::SCALE);
|
||||||
|
if (isTranslate || isScale) {
|
||||||
|
bool maskAxis[3] = {false, false, false};
|
||||||
|
const glm::vec3 camPos(glm::inverse(view)[3]);
|
||||||
|
glm::vec3 camToObj = glm::vec3(m[3]) - camPos;
|
||||||
|
const float camDist = glm::length(camToObj);
|
||||||
|
if (camDist > 1e-4f) {
|
||||||
|
camToObj /= camDist;
|
||||||
|
const glm::vec3 viewFwd = -glm::normalize(
|
||||||
|
glm::vec3(view[0][2], view[1][2],
|
||||||
|
view[2][2]));
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
const glm::vec3 axis = gizmo.localSpace
|
||||||
|
? glm::normalize(rotM[i])
|
||||||
|
: glm::vec3(float(i == 0), float(i == 1),
|
||||||
|
float(i == 2));
|
||||||
|
maskAxis[i] = isScale
|
||||||
|
? glm::abs(glm::dot(camToObj, axis))
|
||||||
|
< kScaleMinDot ||
|
||||||
|
glm::abs(glm::dot(viewFwd, axis))
|
||||||
|
> kScaleMaxFwdDot
|
||||||
|
: glm::length(glm::cross(camToObj, axis))
|
||||||
|
< kMoveMinSin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGuizmo::SetAxisMask(maskAxis[0], maskAxis[1],
|
||||||
|
maskAxis[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl 吸附(UE Grid Snap 语义)。库的约定
|
||||||
//(docs/documentation.md):平移 = 世界单位 float[3]
|
//(docs/documentation.md):平移 = 世界单位 float[3]
|
||||||
// 逐轴;旋转 = snap[0] 按度(库内自行 DEG2RAD,传弧度
|
// 逐轴;旋转 = snap[0] 按度(库内自行 DEG2RAD,传弧
|
||||||
// 会被解释成极小步进);缩放 = 只取 snap[0]
|
// 度会被解释成极小步进);缩放 = 只取 snap[0]
|
||||||
float snap[3] = {0.0f, 0.0f, 0.0f};
|
float snap[3] = {0.0f, 0.0f, 0.0f};
|
||||||
const bool snapping = ImGui::GetIO().KeyCtrl;
|
const bool snapping = ImGui::GetIO().KeyCtrl;
|
||||||
if (snapping) {
|
if (snapping) {
|
||||||
|
|||||||
+14
-166
@@ -1,15 +1,18 @@
|
|||||||
//
|
//
|
||||||
// Created by atdunbg on 2026/9/20.
|
// Created by atdunbg on 2026/9/20.
|
||||||
//
|
//
|
||||||
// SandboxApp:第一个基于 Application 框架的程序。
|
// SandboxApp:基于 Application 框架的编辑器宿主程序。
|
||||||
//
|
//
|
||||||
// main 的职责边界:
|
// main 的职责边界:
|
||||||
// - 定义 Application 派生类并运行它
|
// - 定义 Application 派生类并运行它
|
||||||
// - "程序逻辑":场景搭建(实体/组件)、逐帧更新、事件响应、
|
// - 编辑器形态驱动 EditorCamera(Scene view 专用相机,非场景对象)
|
||||||
// 异步环境加载
|
// - 异步环境加载、全局热键、输入归属门控
|
||||||
// - UI 全部委托 App/editor/ 的 docking Editor(本文件不再出现
|
// - UI 全部委托 App/editor/ 的 docking Editor(本文件不再出现
|
||||||
// 任何 ImGui 控件)
|
// 任何 ImGui 控件)
|
||||||
//
|
//
|
||||||
|
// 场景内容(实体/组件)由编辑器 File 菜单 Save/Load 场景文件管理,
|
||||||
|
// 相机实体属于场景数据、只在游戏形态参与渲染(UE 语义)。
|
||||||
|
//
|
||||||
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理。
|
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理。
|
||||||
//
|
//
|
||||||
|
|
||||||
@@ -18,16 +21,8 @@
|
|||||||
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
|
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
|
||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
#include <Engine/Core/Stats.h>
|
#include <Engine/Core/Stats.h>
|
||||||
#include <Engine/Core/AsyncTask.h>
|
|
||||||
#include <Engine/ECS/Scene.h>
|
|
||||||
#include <Engine/ECS/EntityHandle.h>
|
|
||||||
#include <Engine/ECS/Components.h>
|
|
||||||
#include <Engine/Assets/AssetManager.h>
|
|
||||||
#include <Engine/Renderer/Renderer.h>
|
#include <Engine/Renderer/Renderer.h>
|
||||||
#include <Engine/Renderer/Model.h>
|
#include <Engine/Renderer/EditorCamera.h>
|
||||||
|
|
||||||
#include <glm/glm.hpp>
|
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
|
||||||
|
|
||||||
#include <GLFW/glfw3.h>
|
#include <GLFW/glfw3.h>
|
||||||
|
|
||||||
@@ -35,12 +30,9 @@
|
|||||||
#include "EditorContext.h" // kEnvPaths / kDefaultEnv(与 StatsPanel 共用)
|
#include "EditorContext.h" // kEnvPaths / kDefaultEnv(与 StatsPanel 共用)
|
||||||
|
|
||||||
using namespace Engine;
|
using namespace Engine;
|
||||||
using namespace Engine::ECS;
|
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr const char* kTemplePath = "assets/models/SunTemple/SunTemple.fbx";
|
|
||||||
|
|
||||||
class SandboxApp final : public Application
|
class SandboxApp final : public Application
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -58,7 +50,6 @@ namespace
|
|||||||
|
|
||||||
void OnInit() override
|
void OnInit() override
|
||||||
{
|
{
|
||||||
Scene& scene = GetScene();
|
|
||||||
Renderer& renderer = GetRenderer();
|
Renderer& renderer = GetRenderer();
|
||||||
renderer.SetDisplayMode(kDisplayViewport);
|
renderer.SetDisplayMode(kDisplayViewport);
|
||||||
|
|
||||||
@@ -68,155 +59,17 @@ namespace
|
|||||||
|
|
||||||
// Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
|
// Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
|
||||||
InitEditor();
|
InitEditor();
|
||||||
|
|
||||||
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级),
|
|
||||||
// 其余环境在 Stats 面板里按需 lazy 加载后切换
|
|
||||||
const auto env = GetAssets().LoadEnvironment(kEnvPaths[0]);
|
|
||||||
if (env.Valid()) {
|
|
||||||
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
const auto suntemple = GetAssets().LoadModel(kTemplePath);
|
|
||||||
auto entityhandle = scene.CreateEntity("SunTemple");
|
|
||||||
entityhandle.AddComponent<MeshRendererComponent>(MeshRendererComponent{suntemple});
|
|
||||||
*/
|
|
||||||
|
|
||||||
scene.CreateEntity("test entity");
|
|
||||||
scene.CreateEntity("test entity");
|
|
||||||
scene.CreateEntity("test entity");
|
|
||||||
|
|
||||||
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
|
|
||||||
if (gun.Valid()) {
|
|
||||||
m_Gun = scene.CreateEntity("M1911");
|
|
||||||
m_Gun.AddComponent<MeshRendererComponent>(MeshRendererComponent{gun});
|
|
||||||
m_Gun.AddComponent<AnimationComponent>();
|
|
||||||
auto& gt = m_Gun.GetComponent<TransformComponent>();
|
|
||||||
gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
|
|
||||||
// 物理验证:Dynamic 刚体(默认 Box halfExtents 0.5),
|
|
||||||
// 出生位置悬空 → 应下落至地面静止(Transform 由物理写回)
|
|
||||||
m_Gun.AddComponent<RigidBodyComponent>();
|
|
||||||
} else {
|
|
||||||
ENGINE_WARN("m1911 model not loaded, animation test skipped");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 地面:程序化纯色平面(半边长 15,中心 y=-0.5)+ 静态盒刚体
|
|
||||||
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
|
|
||||||
m_Ground = scene.CreateEntity("Ground");
|
|
||||||
m_Ground.GetComponent<TransformComponent>()
|
|
||||||
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose
|
|
||||||
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
|
|
||||||
auto& groundRb = m_Ground.AddComponent<RigidBodyComponent>();
|
|
||||||
groundRb.type = RigidBodyComponent::BodyType::Static;
|
|
||||||
groundRb.boxHalfExtents = glm::vec3(15.0f, 0.1f, 15.0f); // 平面顶点烘焙 ±15,不能用默认 0.5
|
|
||||||
|
|
||||||
/*
|
|
||||||
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
|
|
||||||
if (gun.Valid()) {
|
|
||||||
m_Gun = scene.CreateEntity("M1911");
|
|
||||||
m_Gun.AddComponent<MeshRendererComponent>(MeshRendererComponent{gun});
|
|
||||||
m_Gun.AddComponent<AnimationComponent>();
|
|
||||||
auto& gt = m_Gun.GetComponent<TransformComponent>();
|
|
||||||
gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
|
|
||||||
} else {
|
|
||||||
ENGINE_WARN("m1911 model not loaded, animation test skipped");
|
|
||||||
}
|
|
||||||
|
|
||||||
int countSize = 0;
|
|
||||||
if (countSize) {
|
|
||||||
// 模型资产(PBR 验证场景:backpack 为旧式贴图资产,
|
|
||||||
// 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5)
|
|
||||||
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
|
|
||||||
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
|
|
||||||
|
|
||||||
for (int i = 0; i < countSize; i ++)
|
|
||||||
{
|
|
||||||
for (int j = 0; j < countSize; j ++)
|
|
||||||
{
|
|
||||||
auto m = scene.CreateEntity();
|
|
||||||
m.AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
|
|
||||||
m.GetComponent<TransformComponent>().SetPosition(glm::vec3{(i - countSize * 0.5) * 2, 0, (j - countSize * 0.5) * 2});
|
|
||||||
m_Spinners.push_back(m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 地面:程序化纯色平面(半边长 15,中心 y=-2)
|
|
||||||
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
|
|
||||||
m_Ground = scene.CreateEntity("Ground");
|
|
||||||
m_Ground.GetComponent<TransformComponent>()
|
|
||||||
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose
|
|
||||||
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 相机实体(primary = 当前渲染视角;位姿存实体 Transform)
|
|
||||||
m_Camera = scene.CreateEntity("MainCamera");
|
|
||||||
auto& camera = m_Camera.AddComponent<CameraComponent>();
|
|
||||||
camera.primary = true;
|
|
||||||
m_Camera.GetComponent<TransformComponent>()
|
|
||||||
.SetPosition(glm::vec3(0.0f, 12.0f, 22.0f));
|
|
||||||
m_Camera.GetComponent<TransformComponent>()
|
|
||||||
.SetRotation(glm::vec3(-25.0f, 0.0f, 0.0f));
|
|
||||||
|
|
||||||
// 灯光实体(第一个方向光 = 阴影光源)
|
|
||||||
m_KeyLight = scene.CreateEntity("KeyLight");
|
|
||||||
m_KeyLight.AddComponent<LightComponent>(
|
|
||||||
LightComponent{
|
|
||||||
.type = LightComponent::Type::Directional,
|
|
||||||
.direction = glm::vec3(-0.5f, -0.6f, -0.35f),
|
|
||||||
.color = glm::vec4(1.0f, 0.95f, 0.9f, 5.0f),
|
|
||||||
.castShadow = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
scene.CreateEntity("FillLight")
|
|
||||||
.AddComponent<LightComponent>(
|
|
||||||
LightComponent{
|
|
||||||
.type = LightComponent::Type::Directional,
|
|
||||||
.direction = glm::vec3(0.5f, -0.5f, 0.3f),
|
|
||||||
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.5f)
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
// 点光源(位置 = 实体 Transform,出生即有;无阴影,距离衰减)
|
|
||||||
auto fire = scene.CreateEntity("FirePitLight");
|
|
||||||
fire.GetComponent<TransformComponent>().SetPosition(glm::vec3(2.0f, 1.5f, 1.0f));
|
|
||||||
fire.AddComponent<LightComponent>(
|
|
||||||
LightComponent{
|
|
||||||
.type = LightComponent::Type::Point,
|
|
||||||
.color = glm::vec4(1.0f, 0.6f, 0.3f, 1.0f), // 暖橙
|
|
||||||
.radius = 8.0f,
|
|
||||||
});
|
|
||||||
|
|
||||||
auto cool = scene.CreateEntity("CoolFillLight");
|
|
||||||
cool.GetComponent<TransformComponent>().SetPosition(glm::vec3(-2.5f, 2.0f, -2.0f));
|
|
||||||
cool.AddComponent<LightComponent>(
|
|
||||||
LightComponent{
|
|
||||||
.type = LightComponent::Type::Point,
|
|
||||||
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.2f), // 冷蓝
|
|
||||||
.radius = 10.0f,
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnUpdate(const FrameTime& frameTime) override
|
void OnUpdate(const FrameTime& frameTime) override
|
||||||
{
|
{
|
||||||
// 相机(WASD/Space/Shift 移动;位姿读写实体 Transform)。
|
// 编辑器形态:驱动编辑器相机(RMB 飞行 / MMB 焦点环绕 /
|
||||||
// 编辑器相机走真实轨:暂停(timeScale=0)时仍可飞行观察
|
// 滚轮推拉)。暂停(timeScale=0)时仍可飞行——unscaled dt。
|
||||||
m_Camera.GetComponent<CameraComponent>()
|
// 游戏形态:相机实体由游戏逻辑驱动(本沙盒无逻辑 = 静止)
|
||||||
.camera.Update(GetWindow(),
|
if (GetRenderer().GetDisplayMode() == kDisplayViewport)
|
||||||
m_Camera.GetComponent<TransformComponent>(),
|
|
||||||
frameTime.UnscaledDt());
|
|
||||||
|
|
||||||
// 旋转动画:编辑 Transform 分量后 Compose 重算 world。
|
|
||||||
// 动画时钟 = Time::Elapsed()(scaled 累计,暂停自动冻结——
|
|
||||||
// 帧历史验证:暂停后 Temporal Diff 视图 2 帧内整屏变黑)
|
|
||||||
for (size_t i = 0; i < m_Spinners.size(); ++i)
|
|
||||||
{
|
{
|
||||||
auto& t = m_Spinners[i].GetComponent<TransformComponent>();
|
GetRenderer().GetEditorCamera().Update(GetWindow(),
|
||||||
t.SetRotation(glm::vec3(
|
frameTime.UnscaledDt());
|
||||||
0.0f, static_cast<f32>(Time::Elapsed()) * 60.0f +
|
|
||||||
static_cast<f32>(i) * 40.0f, 0.0f));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +116,7 @@ namespace
|
|||||||
ctx.scene = &GetScene();
|
ctx.scene = &GetScene();
|
||||||
ctx.assets = &GetAssets();
|
ctx.assets = &GetAssets();
|
||||||
ctx.app = this;
|
ctx.app = this;
|
||||||
|
ctx.editorCamera = &GetRenderer().GetEditorCamera();
|
||||||
ctx.getEnvIndex = [this] { return m_EnvIndex; };
|
ctx.getEnvIndex = [this] { return m_EnvIndex; };
|
||||||
ctx.setEnvIndex = [this](int index) { m_EnvIndex = index; };
|
ctx.setEnvIndex = [this](int index) { m_EnvIndex = index; };
|
||||||
// 继承原沙盒默认:初始暂停(StatsPanel 的 Pause Simulation 开关恢复)
|
// 继承原沙盒默认:初始暂停(StatsPanel 的 Pause Simulation 开关恢复)
|
||||||
@@ -281,12 +135,6 @@ namespace
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>();
|
std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>();
|
||||||
|
|
||||||
EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空
|
|
||||||
EntityHandle m_KeyLight{};
|
|
||||||
EntityHandle m_Ground{};
|
|
||||||
EntityHandle m_Gun{}; // 骨骼动画验证实体(M1911)
|
|
||||||
std::vector<EntityHandle> m_Spinners;
|
|
||||||
int m_EnvIndex = kDefaultEnv;
|
int m_EnvIndex = kDefaultEnv;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// Spinner.cpp:C++ 原生脚本示范——Y 轴匀速自转。
|
||||||
|
//
|
||||||
|
// 写脚本三步:
|
||||||
|
// 1. 新建 .cpp 放本目录(CMake 自动进构建)
|
||||||
|
// 2. 继承 ScriptBehavior,覆写 Name/OnCreate/OnUpdate/OnDestroy
|
||||||
|
// 3. 文件级 ENGINE_REGISTER_SCRIPT(类名) 自注册——Inspector 下拉
|
||||||
|
// 与场景序列化按该名解析
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <Engine/Scripting/Script.h>
|
||||||
|
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class Spinner final : public Engine::Scripting::ScriptBehavior {
|
||||||
|
public:
|
||||||
|
const char* Name() const override { return "Spinner"; }
|
||||||
|
|
||||||
|
void OnCreate() override {
|
||||||
|
// 实体与组件已就位;需要别的组件时用 Get<T>()(不存在会炸)
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnUpdate(float dt) override {
|
||||||
|
// 四元数复合绕 Y(避免每帧欧拉↔四元数往返累积失真)
|
||||||
|
|
||||||
|
/*
|
||||||
|
const glm::quat step = glm::angleAxis(
|
||||||
|
glm::radians(m_Speed * dt), glm::vec3(0.0f, 1.0f, 0.0f));
|
||||||
|
Transform().SetRotationQuat(step * Transform().GetRotationQuat());
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
float m_Speed = 90.0f; // 度/秒
|
||||||
|
};
|
||||||
|
|
||||||
|
static const bool s_Spinner_registered = [] {
|
||||||
|
::Engine::Scripting::ScriptRegistry::Register(
|
||||||
|
"Spinner", []() -> ::Engine::Scope<::Engine::Scripting::ScriptBehavior> {
|
||||||
|
return ::Engine::CreateScope<Spinner>();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}();
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// WASDMove.cpp:WASD 控制实体移动(相机水平朝向参考,UE gameplay 语义)。
|
||||||
|
//
|
||||||
|
// 移动语义分派:
|
||||||
|
// - Dynamic 刚体 → 走物理速度门面(每帧喂水平速度,Y 归物理——
|
||||||
|
// 重力/跳跃不受影响;松键喂零速 = 立即停,Unity velocity 语义)
|
||||||
|
// - 无刚体 → 直接平移 Transform
|
||||||
|
//
|
||||||
|
// 编辑器形态键位天然不冲突:RMB 按住 = 相机吃 WASD 飞行(脚本也读
|
||||||
|
// 到按键 → 同时在动,测移动时松开 RMB 即可);游戏形态 WASD 常开。
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <Engine/Scripting/Script.h>
|
||||||
|
#include <Engine/Core/Application.h>
|
||||||
|
#include <Engine/Core/Window.h>
|
||||||
|
#include <Engine/Renderer/Renderer.h>
|
||||||
|
#include <Engine/Renderer/EditorCamera.h>
|
||||||
|
#include <Engine/Physics/PhysicsWorld.h>
|
||||||
|
|
||||||
|
#include <GLFW/glfw3.h>
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class WASDMove final : public Engine::Scripting::ScriptBehavior {
|
||||||
|
public:
|
||||||
|
const char* Name() const override { return "WASDMove"; }
|
||||||
|
|
||||||
|
void OnUpdate(float dt) override {
|
||||||
|
// 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene
|
||||||
|
// view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)
|
||||||
|
const glm::quat cq = App().GetRenderer()
|
||||||
|
.GetEditorCamera()
|
||||||
|
.GetTransform()
|
||||||
|
.GetRotationQuat();
|
||||||
|
const glm::vec3 fwd = cq * glm::vec3(0.0f, 0.0f, -1.0f);
|
||||||
|
const glm::vec3 right = cq * glm::vec3(1.0f, 0.0f, 0.0f);
|
||||||
|
|
||||||
|
auto& win = App().GetWindow();
|
||||||
|
glm::vec3 move(0.0f);
|
||||||
|
if (win.IsKeyPressed(GLFW_KEY_W)) move += fwd;
|
||||||
|
if (win.IsKeyPressed(GLFW_KEY_S)) move -= fwd;
|
||||||
|
if (win.IsKeyPressed(GLFW_KEY_D)) move += right;
|
||||||
|
if (win.IsKeyPressed(GLFW_KEY_A)) move -= right;
|
||||||
|
move.y = 0.0f;
|
||||||
|
if (glm::length(move) > 1e-4f) {
|
||||||
|
move = glm::normalize(move);
|
||||||
|
}
|
||||||
|
const glm::vec3 vel = move * m_Speed;
|
||||||
|
|
||||||
|
if (auto* rb = TryGetComponent<Engine::ECS::RigidBodyComponent>();
|
||||||
|
rb != nullptr && rb->bodyValid &&
|
||||||
|
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
|
||||||
|
// 物理驱动:只覆写水平速度,Y 保留物理积分离散结果
|
||||||
|
//(整体喂 0 会抵消重力 → 空中物体悬浮)
|
||||||
|
auto& phy = App().GetPhysics();
|
||||||
|
const glm::vec3 cur = phy.GetLinearVelocity(EntityBits());
|
||||||
|
phy.SetLinearVelocity(EntityBits(), {vel.x, cur.y, vel.z});
|
||||||
|
} else {
|
||||||
|
Transform().SetPosition(Transform().GetPosition() + vel * dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
float m_Speed = 5.0f; // m/s
|
||||||
|
};
|
||||||
|
|
||||||
|
ENGINE_REGISTER_SCRIPT(WASDMove)
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -59,6 +59,13 @@ set(ASSIMP_BUILD_STL_IMPORTER ON CACHE BOOL "" FORCE)
|
|||||||
set(ASSIMP_BUILD_PLY_IMPORTER ON CACHE BOOL "" FORCE)
|
set(ASSIMP_BUILD_PLY_IMPORTER ON CACHE BOOL "" FORCE)
|
||||||
add_subdirectory(Lib/assimp)
|
add_subdirectory(Lib/assimp)
|
||||||
|
|
||||||
|
# ---- yaml-cpp(submodule,场景序列化;只编静态库本体)----
|
||||||
|
set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "" FORCE)
|
||||||
|
set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||||
|
set(YAML_CPP_INSTALL OFF CACHE BOOL "" FORCE)
|
||||||
|
add_subdirectory(Lib/yaml-cpp)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Vulkan
|
# Vulkan
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ target_link_libraries(Engine PUBLIC
|
|||||||
assimp::assimp
|
assimp::assimp
|
||||||
meshoptimizer
|
meshoptimizer
|
||||||
Jolt::Jolt
|
Jolt::Jolt
|
||||||
|
yaml-cpp::yaml-cpp
|
||||||
imgui
|
imgui
|
||||||
ImGuizmo
|
ImGuizmo
|
||||||
tracy)
|
tracy)
|
||||||
|
|||||||
@@ -172,14 +172,31 @@ namespace Engine
|
|||||||
|
|
||||||
AssetCache<Model>& Models() { return m_Models; }
|
AssetCache<Model>& Models() { return m_Models; }
|
||||||
|
|
||||||
|
// ---- 资产 GUID 注册表(UE AssetRegistry 模式:中央注册表)----
|
||||||
|
// 磁盘路径资产持久映射 GUID→路径(存 assets/asset_registry.yaml),
|
||||||
|
// 场景文件以 GUID 为主键引用资产——重命名/移动资产不断引用,
|
||||||
|
// Path 仅作人工可读提示与解析兜底。procedural:// 伪路径(参数
|
||||||
|
// 确定性生成,无需稳定身份)不参与
|
||||||
|
// 查询/分配(新路径分配 GUID 并立即持久化,崩溃安全)
|
||||||
|
u64 GetOrCreateGUID(const std::string& path);
|
||||||
|
// GUID → 路径;未登记返回空串
|
||||||
|
std::string ResolveGUID(u64 guid) const;
|
||||||
|
|
||||||
// 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle
|
// 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle
|
||||||
// 各自一个 AssetCache<T> + 便捷方法,模式与 Model 完全一致。
|
// 各自一个 AssetCache<T> + 便捷方法,模式与 Model 完全一致。
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void LoadGUIDRegistry();
|
||||||
|
void SaveGUIDRegistry() const;
|
||||||
|
|
||||||
IAssetUploader& m_Uploader;
|
IAssetUploader& m_Uploader;
|
||||||
|
|
||||||
AssetCache<Model> m_Models;
|
AssetCache<Model> m_Models;
|
||||||
AssetCache<Cubemap> m_Environments;
|
AssetCache<Cubemap> m_Environments;
|
||||||
|
|
||||||
|
// GUID 注册表(双向);dirty 即时保存,无析构落盘依赖
|
||||||
|
std::unordered_map<u64, std::string> m_GuidToPath;
|
||||||
|
std::unordered_map<std::string, u64> m_PathToGuid;
|
||||||
};
|
};
|
||||||
} // namespace Assets
|
} // namespace Assets
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ namespace Engine {
|
|||||||
class Renderer;
|
class Renderer;
|
||||||
class ImGuiLayer;
|
class ImGuiLayer;
|
||||||
class PhysicsWorld;
|
class PhysicsWorld;
|
||||||
|
namespace Scripting { class ScriptRuntime; }
|
||||||
|
|
||||||
namespace ECS { class Scene; }
|
namespace ECS { class Scene; }
|
||||||
namespace Assets { class AssetManager; }
|
namespace Assets { class AssetManager; }
|
||||||
@@ -73,6 +74,7 @@ namespace Engine {
|
|||||||
ECS::Scene& GetScene() const { return *m_Scene; }
|
ECS::Scene& GetScene() const { return *m_Scene; }
|
||||||
Assets::AssetManager& GetAssets() const { return *m_Assets; }
|
Assets::AssetManager& GetAssets() const { return *m_Assets; }
|
||||||
PhysicsWorld& GetPhysics() const { return *m_Physics; }
|
PhysicsWorld& GetPhysics() const { return *m_Physics; }
|
||||||
|
Scripting::ScriptRuntime& GetScripts() const { return *m_Scripts; }
|
||||||
u64 GetFrameCount() const { return m_FrameCount; }
|
u64 GetFrameCount() const { return m_FrameCount; }
|
||||||
|
|
||||||
// ImGui相关
|
// ImGui相关
|
||||||
@@ -120,6 +122,7 @@ namespace Engine {
|
|||||||
Scope<Renderer> m_Renderer;
|
Scope<Renderer> m_Renderer;
|
||||||
Scope<Assets::AssetManager> m_Assets;
|
Scope<Assets::AssetManager> m_Assets;
|
||||||
Scope<PhysicsWorld> m_Physics; // Jolt 物理(Run 起循环时创建)
|
Scope<PhysicsWorld> m_Physics; // Jolt 物理(Run 起循环时创建)
|
||||||
|
Scope<Scripting::ScriptRuntime> m_Scripts; // 脚本生命周期(Run 起循环时创建)
|
||||||
// 注意成员序:ImGuiLayer 必须后声明(先于 Renderer/Window 析构,
|
// 注意成员序:ImGuiLayer 必须后声明(先于 Renderer/Window 析构,
|
||||||
// 其后端销毁需要 Device 与 GLFWwindow 存活)
|
// 其后端销毁需要 Device 与 GLFWwindow 存活)
|
||||||
Scope<ImGuiLayer> m_ImGuiLayer;
|
Scope<ImGuiLayer> m_ImGuiLayer;
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ namespace Engine {
|
|||||||
bool IsMouseButtonPressed(int button) const;
|
bool IsMouseButtonPressed(int button) const;
|
||||||
void GetMouseDelta(float& dx, float& dy) const;
|
void GetMouseDelta(float& dx, float& dy) const;
|
||||||
|
|
||||||
|
// 轮询式滚轮增量(上行 = +1,一档 ≈1):读后清零。UI 捕获鼠标
|
||||||
|
//(面板悬停)时不累积——滚轮归 ImGui
|
||||||
|
float ConsumeScrollDelta();
|
||||||
|
|
||||||
// 光标捕获(自由视角):DISABLED 隐藏光标出无界增量,NORMAL 恢复。
|
// 光标捕获(自由视角):DISABLED 隐藏光标出无界增量,NORMAL 恢复。
|
||||||
// 重复设置同值无操作;切换后鼠标增量参考点重置(防跳变)
|
// 重复设置同值无操作;切换后鼠标增量参考点重置(防跳变)
|
||||||
void SetCursorCaptured(bool captured);
|
void SetCursorCaptured(bool captured);
|
||||||
@@ -92,6 +96,7 @@ namespace Engine {
|
|||||||
mutable bool m_FirstMouse = true;
|
mutable bool m_FirstMouse = true;
|
||||||
mutable double m_LastMouseX = 0.0;
|
mutable double m_LastMouseX = 0.0;
|
||||||
mutable double m_LastMouseY = 0.0;
|
mutable double m_LastMouseY = 0.0;
|
||||||
|
mutable float m_ScrollAccum = 0.0f; // 滚轮累积(ConsumeScrollDelta)
|
||||||
|
|
||||||
// GLFW 静态回调(通过 window user pointer 找回 this)
|
// GLFW 静态回调(通过 window user pointer 找回 this)
|
||||||
static void FramebufferResizeCallback(GLFWwindow* window, int width, int height);
|
static void FramebufferResizeCallback(GLFWwindow* window, int width, int height);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
class Model;
|
class Model;
|
||||||
@@ -122,12 +123,14 @@ namespace Engine::ECS {
|
|||||||
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
|
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
|
||||||
struct MeshRendererComponent {
|
struct MeshRendererComponent {
|
||||||
ModelRef model;
|
ModelRef model;
|
||||||
|
bool active = true; // 禁用 = 渲染/拾取跳过(Unity enabled / UE SetActive)
|
||||||
};
|
};
|
||||||
|
|
||||||
// 骨骼动画播放状态(纯数据 + 便捷控制)。
|
// 骨骼动画播放状态(纯数据 + 便捷控制)。
|
||||||
// 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者);
|
// 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者);
|
||||||
// ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。
|
// ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。
|
||||||
struct AnimationComponent {
|
struct AnimationComponent {
|
||||||
|
bool active = true; // 禁用 = 暂停采样(按当前帧姿态定格)
|
||||||
int clipIndex = 0; // 当前 clip(Model::GetClips 下标)
|
int clipIndex = 0; // 当前 clip(Model::GetClips 下标)
|
||||||
bool playing = true;
|
bool playing = true;
|
||||||
bool loop = true;
|
bool loop = true;
|
||||||
@@ -157,6 +160,7 @@ namespace Engine::ECS {
|
|||||||
|
|
||||||
// 相机。一个场景中 primary=true 的实体是当前渲染视角。
|
// 相机。一个场景中 primary=true 的实体是当前渲染视角。
|
||||||
struct CameraComponent {
|
struct CameraComponent {
|
||||||
|
bool active = true; // 禁用 = 退出 primary 候选
|
||||||
Camera camera;
|
Camera camera;
|
||||||
bool primary = false;
|
bool primary = false;
|
||||||
};
|
};
|
||||||
@@ -165,6 +169,7 @@ namespace Engine::ECS {
|
|||||||
// Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光)
|
// Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光)
|
||||||
// Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减
|
// Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减
|
||||||
struct LightComponent {
|
struct LightComponent {
|
||||||
|
bool active = true; // 禁用 = 不进光照
|
||||||
enum class Type : u8 { Directional = 0, Point = 1 };
|
enum class Type : u8 { Directional = 0, Point = 1 };
|
||||||
Type type = Type::Directional;
|
Type type = Type::Directional;
|
||||||
glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光
|
glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光
|
||||||
@@ -182,20 +187,44 @@ namespace Engine::ECS {
|
|||||||
// 角色 = CharacterControllerComponent(CharacterVirtual),载具 = VehicleComponent
|
// 角色 = CharacterControllerComponent(CharacterVirtual),载具 = VehicleComponent
|
||||||
// (VehicleConstraint)——都走 PhysicsWorld 门面,不复用本组件。
|
// (VehicleConstraint)——都走 PhysicsWorld 门面,不复用本组件。
|
||||||
struct RigidBodyComponent {
|
struct RigidBodyComponent {
|
||||||
|
bool active = true; // 禁用 = body 销毁/不建(对账自然消化)
|
||||||
enum class BodyType : u8 { Static = 0, Dynamic = 1, Kinematic = 2 };
|
enum class BodyType : u8 { Static = 0, Dynamic = 1, Kinematic = 2 };
|
||||||
// Mesh:用实体 MeshRenderer 的视觉网格建碰撞(Unity MeshCollider
|
// Mesh:用实体 MeshRenderer 的视觉网格建碰撞(Unity MeshCollider
|
||||||
// 语义)——Static → Jolt MeshShape(精确三角网格,不可移动);
|
// 语义)——Static → Jolt MeshShape(精确三角网格,不可移动);
|
||||||
// Dynamic/Kinematic → ConvexHullShape(凸包近似,MeshShape 太贵
|
// Dynamic/Kinematic → ConvexHullShape(凸包近似,MeshShape 太贵
|
||||||
// 不能动)。数据源 Mesh 的 CPU 位置副本,无 MeshRenderer 回退 Box
|
// 不能动)。数据源 Mesh 的 CPU 位置副本,无 MeshRenderer 回退 Box
|
||||||
enum class ColliderType : u8 { Box = 0, Sphere = 1, Mesh = 2 };
|
enum class ColliderType : u8 {
|
||||||
|
Box = 0, Sphere = 1, Mesh = 2,
|
||||||
|
Capsule = 3, Cylinder = 4, Compound = 5
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compound 子形状:局部 pos/rot 相对刚体原点,不递归嵌套
|
||||||
|
//(child 不能再是 Compound)。尺寸语义同顶层(× Transform 缩放烘焙)
|
||||||
|
struct CompoundChild {
|
||||||
|
enum class Shape : u8 { Box = 0, Sphere = 1, Capsule = 2 };
|
||||||
|
Shape shape = Shape::Box;
|
||||||
|
glm::vec3 boxHalfExtents{0.25f, 0.25f, 0.25f};
|
||||||
|
f32 radius = 0.25f; // Sphere 半径 / Capsule 半径
|
||||||
|
f32 capsuleHalfHeight = 0.25f; // 仅 Capsule:圆柱段半高
|
||||||
|
glm::vec3 position{0.0f};
|
||||||
|
glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f};
|
||||||
|
bool operator==(const CompoundChild&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
BodyType type = BodyType::Dynamic; // Unity 语义:加 Rigidbody 即受物理驱动
|
BodyType type = BodyType::Dynamic; // Unity 语义:加 Rigidbody 即受物理驱动
|
||||||
ColliderType collider = ColliderType::Box;
|
ColliderType collider = ColliderType::Box;
|
||||||
|
|
||||||
// 碰撞体尺寸(× Transform 缩放,Unity BoxCollider.size 语义)——
|
// 碰撞体尺寸(× Transform 缩放,Unity BoxCollider.size 语义)——
|
||||||
// 程序化模型顶点烘焙尺寸,不能从 scale 反推
|
// 程序化模型顶点烘焙尺寸,不能从 scale 反推。
|
||||||
|
// Capsule/Cylinder 的"半高"指圆柱段半高(不含半球端盖),总高 =
|
||||||
|
// 2*(halfHeight + radius);Jolt 两 ctor 参数序相反见 PhysicsWorld
|
||||||
glm::vec3 boxHalfExtents{0.5f, 0.5f, 0.5f};
|
glm::vec3 boxHalfExtents{0.5f, 0.5f, 0.5f};
|
||||||
f32 sphereRadius = 0.5f;
|
f32 sphereRadius = 0.5f;
|
||||||
|
f32 capsuleRadius = 0.5f; // Unity CapsuleCollider 默认值同构
|
||||||
|
f32 capsuleHalfHeight = 0.5f;
|
||||||
|
f32 cylinderRadius = 0.5f;
|
||||||
|
f32 cylinderHalfHeight = 0.5f;
|
||||||
|
std::vector<CompoundChild> compound; // 仅 ColliderType::Compound
|
||||||
|
|
||||||
// 动态体参数
|
// 动态体参数
|
||||||
f32 mass = 1.0f;
|
f32 mass = 1.0f;
|
||||||
@@ -203,8 +232,9 @@ namespace Engine::ECS {
|
|||||||
f32 friction = 0.5f;
|
f32 friction = 0.5f;
|
||||||
f32 restitution = 0.2f;
|
f32 restitution = 0.2f;
|
||||||
|
|
||||||
// 传感器(Unity isTrigger 语义):只产生 Added/Removed 事件,无碰撞解算
|
// 触发器(Unity isTrigger / UE Overlap Events 语义):只产生
|
||||||
bool isSensor = false;
|
// Added/Removed 事件,无碰撞解算;底层 Jolt sensor
|
||||||
|
bool isTrigger = false;
|
||||||
|
|
||||||
// 运行时(PhysicsWorld 维护,勿手改)
|
// 运行时(PhysicsWorld 维护,勿手改)
|
||||||
u32 bodyId = 0; // JPH::BodyID 裸位
|
u32 bodyId = 0; // JPH::BodyID 裸位
|
||||||
@@ -212,6 +242,56 @@ namespace Engine::ECS {
|
|||||||
glm::vec3 velocity{0.0f}; // 只读调试视图:线速度(m/s)
|
glm::vec3 velocity{0.0f}; // 只读调试视图:线速度(m/s)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 约束/关节(Jolt TwoBodyConstraint 家族)。纯数据 POD 零 Jolt 依赖;
|
||||||
|
// 创建/对账在 PhysicsWorld::SyncConstraints(kPhyDbgConstraints 出图元)。
|
||||||
|
// target 存 UUID(跨存档稳定),惰性解析——SyncScene 每帧按 UUID 反查
|
||||||
|
// 实体:序列化无需两遍解析,UUID 再生后自然失联,加载顺序无关
|
||||||
|
struct JointComponent {
|
||||||
|
bool active = true; // 禁用 = 约束销毁/不建(对账自然消化)
|
||||||
|
enum class Type : u8 { Fixed = 0, Hinge = 1, Ball = 2 };
|
||||||
|
Type type = Type::Fixed;
|
||||||
|
UUID target{}; // 目标实体(0 = 未设置,不建约束)
|
||||||
|
|
||||||
|
// 锚点:各体 Transform 局部系(Unity connectedAnchor 语义,
|
||||||
|
// × 本体缩放烘焙)。Fixed/Hinge/Ball 都连这两点
|
||||||
|
glm::vec3 anchorA{0.0f}; // 本体(挂组件的实体)
|
||||||
|
glm::vec3 anchorB{0.0f}; // 目标体
|
||||||
|
|
||||||
|
// Hinge 专用:转轴(本体局部系,方向量不吃缩放)+ 角度限位(度)
|
||||||
|
glm::vec3 hingeAxis{1.0f, 0.0f, 0.0f};
|
||||||
|
bool enableLimit = false;
|
||||||
|
f32 limitMin = -90.0f; // Jolt 语义 min∈[-180,0] max∈[0,180]
|
||||||
|
f32 limitMax = 90.0f;
|
||||||
|
|
||||||
|
bool operator==(const JointComponent&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 角色控制器参数(Jolt CharacterVirtual 扫掠运动学,非刚体——不受
|
||||||
|
// 物理模拟驱动,游戏逻辑经 PhysicsWorld 门面喂速度/跳跃)。本组件
|
||||||
|
// 只存参数(序列化:存参数不存状态),速度/着地等运行时状态在
|
||||||
|
// PhysicsWorld 的 CharacterController 里(对账重建)
|
||||||
|
struct CharacterControllerComponent {
|
||||||
|
bool active = true; // 禁用 = 角色移除(重启用按当前 Transform 重建)
|
||||||
|
f32 radius = 0.3f; // 胶囊半径
|
||||||
|
f32 capsuleHalfHeight = 0.4f; // 圆柱段半高(总高 = 2*(hh+r))
|
||||||
|
f32 mass = 70.0f;
|
||||||
|
f32 maxSlopeAngle = 45.0f; // 度:可站立的最大坡角
|
||||||
|
f32 stepHeight = 0.4f; // 台阶自动攀爬高(WalkStairs)
|
||||||
|
|
||||||
|
bool operator==(const CharacterControllerComponent&) const = default;
|
||||||
|
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
|
||||||
|
};
|
||||||
|
|
||||||
|
// 脚本组件:只存脚本类名(注册表键,POD 可序列化)。C++ 原生脚本经
|
||||||
|
// ENGINE_REGISTER_SCRIPT 自注册;将来 Lua/C# 脚本同键解析。
|
||||||
|
// 实例生命周期由 ScriptRuntime 对账管理(不进组件,保持可拷贝)
|
||||||
|
struct ScriptComponent {
|
||||||
|
bool active = true; // 禁用 = 实例保留但停 OnUpdate(Unity disable)
|
||||||
|
std::string scriptName;
|
||||||
|
|
||||||
|
bool operator==(const ScriptComponent&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace Engine::ECS
|
} // namespace Engine::ECS
|
||||||
|
|
||||||
#endif //HELLOVULKAN_COMPONENTS_H
|
#endif //HELLOVULKAN_COMPONENTS_H
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ namespace Engine::ECS {
|
|||||||
bool DrawSkybox = true;
|
bool DrawSkybox = true;
|
||||||
};
|
};
|
||||||
Settings& GetSettings() { return m_Settings; }
|
Settings& GetSettings() { return m_Settings; }
|
||||||
|
const Settings& GetSettings() const { return m_Settings; } // 序列化只读
|
||||||
|
|
||||||
// ---- 实体 ----
|
// ---- 实体 ----
|
||||||
|
|
||||||
@@ -54,6 +55,15 @@ namespace Engine::ECS {
|
|||||||
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
|
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
|
||||||
void FlushDestroyQueue(); // 每帧末尾由 Application 调用
|
void FlushDestroyQueue(); // 每帧末尾由 Application 调用
|
||||||
|
|
||||||
|
// 整场景换血(序列化加载):立即销毁全部实体并清空延迟队列。
|
||||||
|
// 仅用于"校验通过后的原子替换"——调用方持有的 Entity/EntityHandle
|
||||||
|
// 全部悬空,必须自行重解析(GetEntityHandle 可从裸 entity 重建)
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
// 裸 entity → 句柄表皮(EntityHandle 构造私有 + friend Scene;
|
||||||
|
// 序列化加载后应用层重解析悬空句柄用)。无效 entity 返回空句柄
|
||||||
|
EntityHandle GetEntityHandle(Entity e);
|
||||||
|
|
||||||
// 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update
|
// 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update
|
||||||
// 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用,
|
// 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用,
|
||||||
// 用户代码永远不需要手动 Compose
|
// 用户代码永远不需要手动 Compose
|
||||||
@@ -64,6 +74,7 @@ namespace Engine::ECS {
|
|||||||
Entity FindEntityByUUID(UUID uuid);
|
Entity FindEntityByUUID(UUID uuid);
|
||||||
|
|
||||||
Registry& GetRegistry() { return m_Registry; }
|
Registry& GetRegistry() { return m_Registry; }
|
||||||
|
const Registry& GetRegistry() const { return m_Registry; } // 序列化等只读遍历
|
||||||
|
|
||||||
// ---- 组件便捷接口(转发 entt)----
|
// ---- 组件便捷接口(转发 entt)----
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
//
|
||||||
|
// CharacterController:单实体角色控制器(Jolt CharacterVirtual 后端)。
|
||||||
|
//
|
||||||
|
// 非刚体、扫掠运动学:CharacterVirtual 不注册进 PhysicsSystem,位置由
|
||||||
|
// 本类每步扫掠推进(撞墙滑动/斜坡限制/贴地/上台阶),其他物理对象
|
||||||
|
// "看不到"它(无碰撞解算,仅查询)。由 PhysicsWorld 对账创建/销毁并
|
||||||
|
// 在 PhysicsSystem::Update 之后驱动(Jolt 官方建议序)。
|
||||||
|
//
|
||||||
|
// 速度语义(游戏逻辑职责分工):
|
||||||
|
// - 水平移动:游戏逻辑每帧 SetCharacterVelocity 喂意图速度
|
||||||
|
// - 垂直分量:本类自动积分重力(着地清零向下分量);Jump 置垂直速度
|
||||||
|
// (Unity CharacterController.Move + gravity 手动积分同构)
|
||||||
|
//
|
||||||
|
// 实体原点 = 脚底(胶囊底端经 mShapeOffset 抬升到原点上方)。
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef HELLOVULKAN_CHARACTERCONTROLLER_H
|
||||||
|
#define HELLOVULKAN_CHARACTERCONTROLLER_H
|
||||||
|
|
||||||
|
#include <Engine/Core/Types.h>
|
||||||
|
|
||||||
|
#include <Jolt/Jolt.h>
|
||||||
|
#include <Jolt/Physics/PhysicsSystem.h>
|
||||||
|
#include <Jolt/Physics/Character/CharacterVirtual.h>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
namespace Engine {
|
||||||
|
|
||||||
|
class CharacterController {
|
||||||
|
public:
|
||||||
|
// 创建参数快照(变更检测 → 重建 CharacterVirtual)
|
||||||
|
struct Params {
|
||||||
|
f32 radius = 0.3f; // 胶囊半径
|
||||||
|
f32 halfHeight = 0.4f; // 圆柱段半高
|
||||||
|
f32 mass = 70.0f;
|
||||||
|
f32 maxSlopeAngle = 45.0f; // 度
|
||||||
|
f32 stepHeight = 0.4f; // 台阶攀爬高
|
||||||
|
|
||||||
|
bool operator==(const Params&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
CharacterController(JPH::PhysicsSystem& system, const Params& params,
|
||||||
|
const glm::vec3& position);
|
||||||
|
~CharacterController() = default; // Ref 引用计数自动释放
|
||||||
|
|
||||||
|
// 每物理步调用:重力积分 + ExtendedUpdate(扫掠滑动 + 贴地 +
|
||||||
|
// 上台阶)。allocator 与 PhysicsSystem::Update 串行复用同一池
|
||||||
|
void Update(f32 dt, const JPH::Vec3& gravity,
|
||||||
|
JPH::TempAllocator& allocator);
|
||||||
|
|
||||||
|
// ---- 游戏逻辑门面(PhysicsWorld 转发)----
|
||||||
|
void SetLinearVelocity(const glm::vec3& velocity);
|
||||||
|
glm::vec3 GetLinearVelocity() const;
|
||||||
|
void Jump(f32 speed); // 垂直速度置 speed(着地方向才有效)
|
||||||
|
bool IsGrounded() const;
|
||||||
|
glm::vec3 GetPosition() const;
|
||||||
|
void Teleport(const glm::vec3& position) const; // 编辑器拖动
|
||||||
|
|
||||||
|
const Params& GetParams() const { return m_Params; }
|
||||||
|
|
||||||
|
// 调试渲染:非 body,PhysicsSystem::DrawBodies 画不到,须手动
|
||||||
|
// (脚本在物理位姿/脚底原点处画胶囊线框)
|
||||||
|
void DebugDraw(JPH::DebugRenderer& renderer) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Params m_Params;
|
||||||
|
JPH::Ref<JPH::CharacterVirtual> m_Character;
|
||||||
|
JPH::CharacterVirtual::ExtendedUpdateSettings m_Ext;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Engine
|
||||||
|
|
||||||
|
#endif //HELLOVULKAN_CHARACTERCONTROLLER_H
|
||||||
@@ -25,10 +25,13 @@
|
|||||||
#include <Engine/Physics/PhysicsDebugDraw.h>
|
#include <Engine/Physics/PhysicsDebugDraw.h>
|
||||||
#include <Engine/Physics/PhysicsEvents.h>
|
#include <Engine/Physics/PhysicsEvents.h>
|
||||||
#include <Engine/Physics/PhysicsRay.h>
|
#include <Engine/Physics/PhysicsRay.h>
|
||||||
|
#include <Engine/Physics/CharacterController.h>
|
||||||
|
#include <Engine/ECS/Components.h> // BodyRecord 快照存 CompoundChild(完整类型)
|
||||||
|
|
||||||
#include <Jolt/Jolt.h>
|
#include <Jolt/Jolt.h>
|
||||||
#include <Jolt/Physics/PhysicsSystem.h>
|
#include <Jolt/Physics/PhysicsSystem.h>
|
||||||
#include <Jolt/Physics/Body/BodyID.h>
|
#include <Jolt/Physics/Body/BodyID.h>
|
||||||
|
#include <Jolt/Physics/Constraints/Constraint.h> // JPH::Ref<Constraint>
|
||||||
#include <Jolt/Core/JobSystemThreadPool.h>
|
#include <Jolt/Core/JobSystemThreadPool.h>
|
||||||
#include <Jolt/Core/TempAllocator.h>
|
#include <Jolt/Core/TempAllocator.h>
|
||||||
|
|
||||||
@@ -75,6 +78,27 @@ namespace Engine {
|
|||||||
bool RayCast(const glm::vec3& origin, const glm::vec3& dir,
|
bool RayCast(const glm::vec3& origin, const glm::vec3& dir,
|
||||||
f32 maxDist, RayHit& outHit) const;
|
f32 maxDist, RayHit& outHit) const;
|
||||||
|
|
||||||
|
// ---- 刚体驱动(游戏逻辑/编辑器调试;按实体位模式寻址)----
|
||||||
|
// Jolt 力在每步模拟后清零:游戏逻辑每帧持续施加 = 正确用法;
|
||||||
|
// 冲量一次性改变动量(自动唤醒休眠体);速度直写会打断自然
|
||||||
|
// 模拟(Unity 同语义)。body 不存在或类型不符时静默忽略:
|
||||||
|
// 力/扭矩/冲量仅动态体,速度直写动态+运动学体
|
||||||
|
void AddForce(u32 entityBits, const glm::vec3& force);
|
||||||
|
void AddImpulse(u32 entityBits, const glm::vec3& impulse);
|
||||||
|
void AddTorque(u32 entityBits, const glm::vec3& torque);
|
||||||
|
void SetLinearVelocity(u32 entityBits, const glm::vec3& velocity);
|
||||||
|
void SetAngularVelocity(u32 entityBits, const glm::vec3& angularVelocity);
|
||||||
|
// 无 body / 静态体返回零向量
|
||||||
|
glm::vec3 GetLinearVelocity(u32 entityBits) const;
|
||||||
|
|
||||||
|
// ---- 角色控制器(游戏逻辑门面;实体无 CharacterControllerComponent
|
||||||
|
// 或未就绪时静默忽略)----
|
||||||
|
// 速度直写喂意图(水平移动);垂直分量由 Update 自动积分重力
|
||||||
|
void SetCharacterVelocity(u32 entityBits, const glm::vec3& velocity);
|
||||||
|
void Jump(u32 entityBits, f32 speed); // 垂直速度置 speed
|
||||||
|
bool IsCharacterGrounded(u32 entityBits) const;
|
||||||
|
glm::vec3 GetCharacterVelocity(u32 entityBits) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// 创建时缓存的参数(变更检测 → 重建 body)
|
// 创建时缓存的参数(变更检测 → 重建 body)
|
||||||
struct BodyRecord {
|
struct BodyRecord {
|
||||||
@@ -82,9 +106,11 @@ namespace Engine {
|
|||||||
u8 type = 0; // RigidBodyComponent::BodyType
|
u8 type = 0; // RigidBodyComponent::BodyType
|
||||||
u8 collider = 0; // RigidBodyComponent::ColliderType
|
u8 collider = 0; // RigidBodyComponent::ColliderType
|
||||||
glm::vec3 colliderSize{0.0f}; // 碰撞体尺寸缓存(box 半尺寸)
|
glm::vec3 colliderSize{0.0f}; // 碰撞体尺寸缓存(box 半尺寸)
|
||||||
f32 radius = 0.0f; // 球半径缓存
|
f32 radius = 0.0f; // 球/Capsule/Cylinder 半径(collider 决定语义)
|
||||||
|
f32 halfHeight = 0.0f; // Capsule/Cylinder 圆柱段半高
|
||||||
|
std::vector<ECS::RigidBodyComponent::CompoundChild> compound; // 子形状快照
|
||||||
f32 mass = 0.0f;
|
f32 mass = 0.0f;
|
||||||
bool isSensor = false; // 传感器标记(变更 → 重建 body)
|
bool isTrigger = false; // 触发器标记(变更 → 重建 body)
|
||||||
glm::vec3 lastPos{0.0f}; // 静态/运动体编辑器拖动检测
|
glm::vec3 lastPos{0.0f}; // 静态/运动体编辑器拖动检测
|
||||||
glm::quat lastRot{1.0f, 0.0f, 0.0f, 0.0f}; // 同上(quat 直比,无欧拉往返)
|
glm::quat lastRot{1.0f, 0.0f, 0.0f, 0.0f}; // 同上(quat 直比,无欧拉往返)
|
||||||
// 加速度箭头用:上一步速度(定步长差分)与平滑后的加速度
|
// 加速度箭头用:上一步速度(定步长差分)与平滑后的加速度
|
||||||
@@ -93,20 +119,54 @@ namespace Engine {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void SyncScene(); // 对账:孤儿清理 / 创建 / 参数同步
|
void SyncScene(); // 对账:孤儿清理 / 创建 / 参数同步
|
||||||
void Writeback() const; // 动态体 → TransformComponent(标脏)
|
void Writeback(); // 动态体 → TransformComponent(标脏)+ lastPos/lastRot 对账基线
|
||||||
void UpdateAccels(f32 dt); // 速度差分 → BodyRecord.lastAccel(step 率)
|
void UpdateAccels(f32 dt); // 速度差分 → BodyRecord.lastAccel(step 率)
|
||||||
void CollectDebugData(); // 按 m_DebugBits 收集调试图元(渲染帧率)
|
void CollectDebugData(); // 按 m_DebugBits 收集调试图元(渲染帧率)
|
||||||
void CreateBody(u32 entityBits, ECS::RigidBodyComponent& rb,
|
void CreateBody(u32 entityBits, ECS::RigidBodyComponent& rb,
|
||||||
ECS::TransformComponent& tr);
|
ECS::TransformComponent& tr);
|
||||||
|
// 约束对账:owner 孤儿清理 / body 重建传染失效 / 建缺失与变更重建
|
||||||
|
// invalidated = 本帧 SyncScene 中被销毁或重建 body 的实体位模式
|
||||||
|
void SyncConstraints(const std::vector<u32>& invalidated);
|
||||||
|
void CreateConstraint(u32 ownerBits, u32 targetBits,
|
||||||
|
const ECS::JointComponent& jc);
|
||||||
|
void RemoveConstraint(u32 ownerBits);
|
||||||
|
// 角色对账:孤儿清理 / 参数变更重建 / 编辑器拖动传送。
|
||||||
|
// 角色非刚体(不进 PhysicsSystem),无 body 清理负担
|
||||||
|
void SyncCharacters();
|
||||||
|
// 每物理步(PhysicsSystem::Update 之后,Jolt 官方建议序):
|
||||||
|
// 重力积分 + 扫掠 → 写回 TransformComponent(标脏)
|
||||||
|
void UpdateCharacters(f32 dt);
|
||||||
// Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape /
|
// Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape /
|
||||||
// Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box)
|
// Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box)
|
||||||
JPH::RefConst<JPH::Shape> BuildMeshShape(u32 entityBits,
|
JPH::RefConst<JPH::Shape> BuildMeshShape(u32 entityBits,
|
||||||
ECS::RigidBodyComponent& rb,
|
ECS::RigidBodyComponent& rb,
|
||||||
ECS::TransformComponent& tr);
|
ECS::TransformComponent& tr);
|
||||||
|
// Compound 碰撞体:子形状列表 → Jolt StaticCompoundShape。
|
||||||
|
// 空 children 或构建失败返回 null(回退 Box)
|
||||||
|
JPH::RefConst<JPH::Shape> BuildCompoundShape(ECS::RigidBodyComponent& rb,
|
||||||
|
ECS::TransformComponent& tr);
|
||||||
void RemoveBody(u32 entityBits);
|
void RemoveBody(u32 entityBits);
|
||||||
|
const BodyRecord* FindRecord(u32 entityBits) const; // 刚体驱动寻址
|
||||||
|
CharacterController* FindCharacter(u32 entityBits); // 角色门面寻址
|
||||||
void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁)
|
void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁)
|
||||||
bool TryBitsByBodyId(const JPH::BodyID& id, u32& outBits) const;
|
bool TryBitsByBodyId(const JPH::BodyID& id, u32& outBits) const;
|
||||||
|
|
||||||
|
// ---- 约束(JointComponent → Jolt TwoBodyConstraint;owner 实体寻址)----
|
||||||
|
// target 惰性解析 UUID;body 重建/销毁 → 引用它的约束失效重建
|
||||||
|
struct ConstraintRecord {
|
||||||
|
JPH::Ref<JPH::Constraint> constraint;
|
||||||
|
ECS::JointComponent snapshot; // 变更检测 → 移除重建
|
||||||
|
u32 targetBits = 0;
|
||||||
|
};
|
||||||
|
std::unordered_map<u32, ConstraintRecord> m_Constraints;
|
||||||
|
|
||||||
|
// ---- 角色(entityBits → CharacterVirtual 封装)----
|
||||||
|
struct CharacterEntry {
|
||||||
|
std::unique_ptr<CharacterController> cc;
|
||||||
|
glm::vec3 lastPos{0.0f}; // 编辑器拖动检测
|
||||||
|
};
|
||||||
|
std::unordered_map<u32, CharacterEntry> m_Characters;
|
||||||
|
|
||||||
ECS::Scene& m_Scene;
|
ECS::Scene& m_Scene;
|
||||||
Assets::AssetManager* m_Assets = nullptr; // Mesh 碰撞体解析 modelRef
|
Assets::AssetManager* m_Assets = nullptr; // Mesh 碰撞体解析 modelRef
|
||||||
bool m_Enabled = true;
|
bool m_Enabled = true;
|
||||||
|
|||||||
@@ -4,9 +4,18 @@
|
|||||||
// Camera:独立的相机模块。
|
// Camera:独立的相机模块。
|
||||||
//
|
//
|
||||||
// 设计(Transform 单一事实源):相机的位置与朝向完全由实体上的
|
// 设计(Transform 单一事实源):相机的位置与朝向完全由实体上的
|
||||||
// TransformComponent 承载——Update 把输入直接写回 transform(末尾
|
// TransformComponent 承载——Update 把输入直接写回 transform,视图
|
||||||
// Compose),视图矩阵 = 实体世界矩阵的逆(本地 -Z 为视线方向)。
|
// 矩阵 = 实体世界矩阵的逆(本地 -Z 为视线方向)。相机类自身只保存
|
||||||
// 相机类自身不保存任何位姿状态,杜绝"位置两套来源"的脱节。
|
// 焦点环绕(pivot orbit)状态(pivot/距离),杜绝位姿两套来源。
|
||||||
|
//
|
||||||
|
// 交互(UE Scene view 语义):
|
||||||
|
// RMB 按住 自由视角:鼠标转向 + WASD/Space/Shift 飞行
|
||||||
|
// (编辑器模式下 RMB 按住期间键盘归相机,W/E/R 不切
|
||||||
|
// Gizmo——键位冲突的解法与 UE 一致)
|
||||||
|
// MMB 拖拽 焦点环绕:绕 pivot 转向,距离不变
|
||||||
|
// Shift+MMB 屏幕平面平移(相机与 pivot 同步移动)
|
||||||
|
// 滚轮 推拉:改环绕距离(有 pivot 时指数步进)
|
||||||
|
// F(面板侧) 焦点选中实体(ViewportPanel 调 FocusOn)
|
||||||
//
|
//
|
||||||
|
|
||||||
#ifndef HELLOVULKAN_CAMERA_H
|
#ifndef HELLOVULKAN_CAMERA_H
|
||||||
@@ -22,9 +31,15 @@ namespace Engine {
|
|||||||
|
|
||||||
class Camera {
|
class Camera {
|
||||||
public:
|
public:
|
||||||
// 输入驱动:WASD/Space/Shift 移动 + 按住右键鼠标视角。
|
// 输入驱动。flyRequiresRMB=true(编辑器模式):移动键仅在 RMB
|
||||||
// 全部写回传入的 transform,有变更才 Compose。
|
// 按住时生效——解决与 Gizmo W/E/R 工具热键的键位冲突(UE 语义:
|
||||||
void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime) const;
|
// 飞行时键盘归相机)。全部写回传入的 transform
|
||||||
|
void Update(Window& window, ECS::TransformComponent& transform,
|
||||||
|
f32 deltaTime, bool flyRequiresRMB = false);
|
||||||
|
|
||||||
|
// 焦点选中(UE frame-selection):pivot 设为 point,相机沿当前
|
||||||
|
// 视线平移对准;环绕距离沿用当前值(初始 5m,滚轮可改)
|
||||||
|
void FocusOn(ECS::TransformComponent& transform, const glm::vec3& point);
|
||||||
|
|
||||||
// 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose)
|
// 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose)
|
||||||
glm::mat4 GetView(const ECS::TransformComponent& transform) const;
|
glm::mat4 GetView(const ECS::TransformComponent& transform) const;
|
||||||
@@ -35,15 +50,24 @@ namespace Engine {
|
|||||||
void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
|
void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
|
||||||
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
|
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
|
||||||
|
|
||||||
// 鼠标视角总开关(默认开)。交互 = 按住右键自由视角(光标捕获),
|
// 相机输入总开关(默认开)。关闭后转向/环绕/推拉/飞行全部静止
|
||||||
// 松开释放光标;右键落在 ImGui 面板上时归 UI 不触发相机
|
|
||||||
void SetMouseLookEnabled(const bool enabled) { m_MouseLookEnabled = enabled; }
|
void SetMouseLookEnabled(const bool enabled) { m_MouseLookEnabled = enabled; }
|
||||||
bool IsMouseLookEnabled() const { return m_MouseLookEnabled; }
|
bool IsMouseLookEnabled() const { return m_MouseLookEnabled; }
|
||||||
|
|
||||||
|
// 清除环绕 pivot(位姿被外部整体改写后 pivot 已失效——下一次
|
||||||
|
// MMB 环绕按新视线自动重建)
|
||||||
|
void ResetOrbit() { m_HasPivot = false; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
f32 m_MoveSpeed = 2.5f;
|
f32 m_MoveSpeed = 2.5f;
|
||||||
f32 m_MouseSensitivity = 0.1f;
|
f32 m_MouseSensitivity = 0.1f;
|
||||||
bool m_MouseLookEnabled = true;
|
bool m_MouseLookEnabled = true;
|
||||||
|
|
||||||
|
// 焦点环绕状态:pivot + 距离。首次环绕无 pivot 时自动取当前
|
||||||
|
// 视线上 m_OrbitDistance 处;FocusOn 显式设置
|
||||||
|
glm::vec3 m_OrbitPivot{0.0f};
|
||||||
|
f32 m_OrbitDistance = 5.0f;
|
||||||
|
bool m_HasPivot = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
//
|
||||||
|
// Created by atdunbg on 2026/10/3.
|
||||||
|
//
|
||||||
|
// EditorCamera:Scene view 专用相机(UE FEditorViewportClient 语义)。
|
||||||
|
//
|
||||||
|
// 与场景相机实体是两个东西——它是视口 UI 状态,不属于场景:
|
||||||
|
// - 位姿自有(内嵌 TransformComponent,无父层级),空场景/无相机
|
||||||
|
// 实体也可用,永在不可删
|
||||||
|
// - 交互复用 Camera 控制器(RMB 飞行 / MMB 环绕 / 滚轮推拉),输入
|
||||||
|
// 直接写回自有 transform——位姿单一事实源不变(transform 即位姿)
|
||||||
|
// - 渲染源分派在 SceneRenderer::BuildFrame:编辑器形态恒用本类,
|
||||||
|
// 游戏形态用场景 primary 相机实体(游戏逻辑所有,本类不参与,
|
||||||
|
// 不 Update 即静止)
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef HELLOVULKAN_EDITORCAMERA_H
|
||||||
|
#define HELLOVULKAN_EDITORCAMERA_H
|
||||||
|
|
||||||
|
#include <Engine/Renderer/Camera.h>
|
||||||
|
#include <Engine/ECS/Components.h>
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
namespace Engine {
|
||||||
|
|
||||||
|
class Window;
|
||||||
|
|
||||||
|
class EditorCamera {
|
||||||
|
public:
|
||||||
|
EditorCamera()
|
||||||
|
{
|
||||||
|
// 出场位姿:俯视场中央(与旧沙盒 MainCamera 出场一致,
|
||||||
|
// 空场景也有可看的初始视角)
|
||||||
|
m_Transform.SetPosition({0.0f, 12.0f, 22.0f});
|
||||||
|
m_Transform.SetRotation({-25.0f, 0.0f, 0.0f});
|
||||||
|
m_Transform.Compose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每帧输入驱动(编辑器形态由 Application 调用;游戏形态不调
|
||||||
|
// 即静止)。flyRequiresRMB 恒 true:编辑器移动键只在 RMB 按住
|
||||||
|
// 时生效(W/E/R 归 Gizmo,UE 语义)。暂停(timeScale=0)时
|
||||||
|
// 仍可飞行——调用方传 unscaled dt
|
||||||
|
void Update(Window& window, f32 deltaTime)
|
||||||
|
{
|
||||||
|
m_Controller.Update(window, m_Transform, deltaTime,
|
||||||
|
/*flyRequiresRMB=*/true);
|
||||||
|
m_Transform.Compose(); // World() 是渲染/Gizmo 读源,随写随算
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转(对齐视口):把相机实体(或任意实体)位姿复制过来,
|
||||||
|
// Scene view 立即从该位置观看(UE "Pilot This Camera" 的
|
||||||
|
// 一次性对齐版)。环绕 pivot 已失效,清除待 MMB 重建
|
||||||
|
void AlignTo(const ECS::TransformComponent& target)
|
||||||
|
{
|
||||||
|
m_Transform.SetPosition(target.GetPosition());
|
||||||
|
m_Transform.SetRotationQuat(target.GetRotationQuat());
|
||||||
|
m_Transform.Compose();
|
||||||
|
m_Controller.ResetOrbit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 焦点选中(F 键):pivot 设为 point,沿当前视线平移对准
|
||||||
|
void FocusOn(const glm::vec3& point)
|
||||||
|
{
|
||||||
|
m_Controller.FocusOn(m_Transform, point);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 视图 = 自有 transform 世界矩阵的逆(要求已 Compose——
|
||||||
|
// Update/AlignTo/ctor 三个写入口都收尾 Compose)
|
||||||
|
glm::mat4 GetView() const { return m_Controller.GetView(m_Transform); }
|
||||||
|
|
||||||
|
ECS::TransformComponent& GetTransform() { return m_Transform; }
|
||||||
|
Camera& GetController() { return m_Controller; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
ECS::TransformComponent m_Transform; // 自有位姿(非场景实体)
|
||||||
|
Camera m_Controller; // 交互控制器(环绕状态在此)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Engine
|
||||||
|
|
||||||
|
#endif //HELLOVULKAN_EDITORCAMERA_H
|
||||||
@@ -28,10 +28,13 @@ namespace Engine
|
|||||||
public:
|
public:
|
||||||
// material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份)
|
// material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份)
|
||||||
// lods[0] 必须为原始索引(与 vertices 对应),各级简化链由解析期生成
|
// lods[0] 必须为原始索引(与 vertices 对应),各级简化链由解析期生成
|
||||||
|
// cpuPositions 非空(蒙皮 mesh)= 物理数据源与包围球改用它
|
||||||
|
//(绑定姿态渲染空间,与视觉同源;vertices.position 是蒙皮原始空间)
|
||||||
Mesh(Device& device, MemoryAllocator& allocator,
|
Mesh(Device& device, MemoryAllocator& allocator,
|
||||||
const std::vector<Vertex>& vertices,
|
const std::vector<Vertex>& vertices,
|
||||||
const std::vector<std::vector<u32>>& lods,
|
const std::vector<std::vector<u32>>& lods,
|
||||||
Material* material);
|
Material* material,
|
||||||
|
const std::vector<glm::vec3>* cpuPositions = nullptr);
|
||||||
~Mesh();
|
~Mesh();
|
||||||
|
|
||||||
Mesh(const Mesh&) = delete;
|
Mesh(const Mesh&) = delete;
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ namespace Engine
|
|||||||
std::array<std::string, Material::kSlotCount> texPaths;
|
std::array<std::string, Material::kSlotCount> texPaths;
|
||||||
MaterialParams params;
|
MaterialParams params;
|
||||||
bool transparent = false; // glTF alphaMode=BLEND(透明绘制队列)
|
bool transparent = false; // glTF alphaMode=BLEND(透明绘制队列)
|
||||||
|
bool skinned = false; // 有骨骼(渲染走 palette 蒙皮)
|
||||||
|
|
||||||
|
// 蒙皮 mesh 专属:绑定姿态渲染空间的位置副本(palette(bind) 按
|
||||||
|
// 顶点权重混合,与 shader 同式)。非空 = Mesh 的物理数据源与
|
||||||
|
// 包围球改用它——FBX 单位换算折在骨骼链上(bind palette ≠ I),
|
||||||
|
// 直接用原始顶点会让物理凸包大一个数量级(M1911 实测 10×)
|
||||||
|
std::vector<glm::vec3> cpuPositions;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- 骨骼动画数据(assimp aiNode/aiAnimation/aiBone 的引擎侧镜像)----
|
// ---- 骨骼动画数据(assimp aiNode/aiAnimation/aiBone 的引擎侧镜像)----
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace Engine {
|
|||||||
class CommandManager;
|
class CommandManager;
|
||||||
class UniformBuffer;
|
class UniformBuffer;
|
||||||
class SceneRenderer;
|
class SceneRenderer;
|
||||||
|
class EditorCamera;
|
||||||
class VulkanAssetUploader;
|
class VulkanAssetUploader;
|
||||||
class Cubemap;
|
class Cubemap;
|
||||||
class IblGenerator;
|
class IblGenerator;
|
||||||
@@ -141,6 +142,9 @@ namespace Engine {
|
|||||||
// ---- 显示模式(Editor 视口 ↔ 全屏直出;帧首同步生效)----
|
// ---- 显示模式(Editor 视口 ↔ 全屏直出;帧首同步生效)----
|
||||||
int GetDisplayMode() const { return m_DesiredDisplayMode; }
|
int GetDisplayMode() const { return m_DesiredDisplayMode; }
|
||||||
void SetDisplayMode(int mode); // cpp 内定义(SceneRenderer 前置声明)
|
void SetDisplayMode(int mode); // cpp 内定义(SceneRenderer 前置声明)
|
||||||
|
// 编辑器相机(Scene view 恒用,与场景相机实体无关)。转发
|
||||||
|
// SceneRenderer 持有的实例:Application 驱动 Update,面板读写位姿
|
||||||
|
EditorCamera& GetEditorCamera(); // cpp 内定义(SceneRenderer 完整类型)
|
||||||
// 当帧场景数据(只读直通)。CPU 消费如视口拾取:反投影矩阵必须
|
// 当帧场景数据(只读直通)。CPU 消费如视口拾取:反投影矩阵必须
|
||||||
// 与渲染同一台相机同一投影(含 [0,1] 深度语义),禁止自行重算
|
// 与渲染同一台相机同一投影(含 [0,1] 深度语义),禁止自行重算
|
||||||
const FrameSceneData& GetFrameSceneData() const; // cpp 内定义
|
const FrameSceneData& GetFrameSceneData() const; // cpp 内定义
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <Engine/Renderer/EditorCamera.h>
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
namespace ECS { class Scene; }
|
namespace ECS { class Scene; }
|
||||||
@@ -35,7 +37,8 @@ namespace Engine {
|
|||||||
|
|
||||||
class SceneRenderer {
|
class SceneRenderer {
|
||||||
public:
|
public:
|
||||||
// 从场景构建当帧渲染数据。无 primary 相机返回 false(渲染跳帧)。
|
// 从场景构建当帧渲染数据。游戏形态无 primary 相机返回 false
|
||||||
|
//(渲染跳帧;编辑器形态恒用编辑器相机,不跳)。
|
||||||
// targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。
|
// targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。
|
||||||
// dt 驱动骨骼动画采样推进(crossfade 计时)。
|
// dt 驱动骨骼动画采样推进(crossfade 计时)。
|
||||||
bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
|
bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
|
||||||
@@ -62,6 +65,9 @@ namespace Engine {
|
|||||||
void SetLodEnabled(bool b) { m_LodEnabled = b; }
|
void SetLodEnabled(bool b) { m_LodEnabled = b; }
|
||||||
void SetDebugView(int v) { m_DebugView = v; }
|
void SetDebugView(int v) { m_DebugView = v; }
|
||||||
void SetDisplayMode(int m) { m_DisplayMode = m; } // kDisplayDirect / kDisplayViewport
|
void SetDisplayMode(int m) { m_DisplayMode = m; } // kDisplayDirect / kDisplayViewport
|
||||||
|
// 编辑器相机(视口 UI 状态,SceneRenderer 持有 = 渲染源在此分派);
|
||||||
|
// Application 驱动 Update,ViewportPanel/Inspector 经 Renderer 门面访问
|
||||||
|
EditorCamera& GetEditorCamera() { return m_EditorCamera; }
|
||||||
// Editor 视口拾取请求(UI 段写入,下一次 BuildFrame 落帧后清除)
|
// Editor 视口拾取请求(UI 段写入,下一次 BuildFrame 落帧后清除)
|
||||||
void SetPickRequest(const glm::vec2& ndc)
|
void SetPickRequest(const glm::vec2& ndc)
|
||||||
{
|
{
|
||||||
@@ -83,6 +89,7 @@ namespace Engine {
|
|||||||
static void PackIndirect(CullResult& view, bool byMaterial);
|
static void PackIndirect(CullResult& view, bool byMaterial);
|
||||||
|
|
||||||
FrameSceneData m_Frame;
|
FrameSceneData m_Frame;
|
||||||
|
EditorCamera m_EditorCamera; // 编辑器相机(视口 UI 状态,见 GetEditorCamera)
|
||||||
PickRequest m_PendingPick{}; // 拾取请求缓冲(BuildFrame 落帧后清除)
|
PickRequest m_PendingPick{}; // 拾取请求缓冲(BuildFrame 落帧后清除)
|
||||||
u32 m_PendingSelected = 0; // 选中实体缓冲(描边)
|
u32 m_PendingSelected = 0; // 选中实体缓冲(描边)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
//
|
||||||
|
// Script.h:C++ 原生脚本设施(行为基类 + 按名注册表)。
|
||||||
|
//
|
||||||
|
// 设计参照 UE:脚本 = 挂在实体上的行为对象 + 统一基类生命周期
|
||||||
|
//(OnCreate/OnUpdate/OnDestroy,BeginPlay/Tick 语义)。引擎帧循环只认
|
||||||
|
// 基类接口——将来接 Lua/C# 时各实现一种 foreign behavior(内部持
|
||||||
|
// VM 状态),引擎零改动。
|
||||||
|
//
|
||||||
|
// 组件只存 scriptName 字符串(POD,序列化天然支持:Lua/C# 同样按名
|
||||||
|
// 解析);实例由 ScriptRuntime 对账式管理(照 PhysicsWorld 模式),
|
||||||
|
// 不进组件——组件可拷贝/可比较。
|
||||||
|
//
|
||||||
|
// 脚本作者注意:
|
||||||
|
// OnCreate/OnUpdate 阶段实体与组件已就位(假定所需组件存在);
|
||||||
|
// OnDestroy 只做自身清理——可能因实体销毁触发,句柄已失效。
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef HELLOVULKAN_SCRIPT_H
|
||||||
|
#define HELLOVULKAN_SCRIPT_H
|
||||||
|
|
||||||
|
#include <Engine/Core/Types.h>
|
||||||
|
#include <Engine/ECS/Components.h>
|
||||||
|
#include <Engine/ECS/Scene.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Engine {
|
||||||
|
|
||||||
|
class Application; // 宿主门面(Window/Renderer/Physics 入口),实现见 ScriptRuntime.cpp
|
||||||
|
|
||||||
|
namespace Scripting {
|
||||||
|
|
||||||
|
class ScriptBehavior {
|
||||||
|
public:
|
||||||
|
virtual ~ScriptBehavior() = default;
|
||||||
|
|
||||||
|
// 注册表键(= 组件 scriptName),须与 ENGINE_REGISTER_SCRIPT 名一致
|
||||||
|
virtual const char* Name() const = 0;
|
||||||
|
|
||||||
|
// 生命周期(吃 scaled dt:编辑器暂停 timeScale=0 时自然停摆)
|
||||||
|
virtual void OnCreate() {}
|
||||||
|
virtual void OnUpdate(f32 dt) {}
|
||||||
|
virtual void OnDestroy() {}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// 便捷访问:绑定实体的 TransformComponent
|
||||||
|
ECS::TransformComponent& Transform() {
|
||||||
|
return m_Scene->GetRegistry().get<ECS::TransformComponent>(m_Entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T& GetComponent() {
|
||||||
|
return m_Scene->GetRegistry().get<T>(m_Entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T* TryGetComponent() {
|
||||||
|
return m_Scene->GetRegistry().try_get<T>(m_Entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定实体的 entityBits(物理门面按键值寻址)
|
||||||
|
u32 EntityBits() const {
|
||||||
|
return static_cast<u32>(entt::to_integral(m_Entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 宿主应用(GetWindow/GetRenderer/GetPhysics/GetScene……)
|
||||||
|
Application& App() const;
|
||||||
|
|
||||||
|
bool Valid() const { return m_Scene != nullptr && m_Entity != entt::null; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class ScriptRuntime;
|
||||||
|
void Bind(ECS::Scene& scene, entt::entity e, Application* app) {
|
||||||
|
m_Scene = &scene;
|
||||||
|
m_Entity = e;
|
||||||
|
m_App = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
ECS::Scene* m_Scene = nullptr;
|
||||||
|
entt::entity m_Entity = entt::null;
|
||||||
|
Application* m_App = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- 按名注册表:脚本类文件级自注册,运行时按名实例化 ----
|
||||||
|
class ScriptRegistry {
|
||||||
|
public:
|
||||||
|
using Factory = Scope<ScriptBehavior> (*)();
|
||||||
|
|
||||||
|
static void Register(const std::string& name, Factory f) {
|
||||||
|
Map()[name] = f;
|
||||||
|
Names().push_back(name); // Inspector 下拉用(保持注册序)
|
||||||
|
}
|
||||||
|
|
||||||
|
static Scope<ScriptBehavior> Create(const std::string& name) {
|
||||||
|
auto& m = Map();
|
||||||
|
const auto it = m.find(name);
|
||||||
|
return it != m.end() ? it->second() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool Contains(const std::string& name) {
|
||||||
|
return Map().count(name) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string>& Names() {
|
||||||
|
static std::vector<std::string> v;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::unordered_map<std::string, Factory>& Map() {
|
||||||
|
static std::unordered_map<std::string, Factory> m;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Scripting
|
||||||
|
} // namespace Engine
|
||||||
|
|
||||||
|
// 文件级自注册:脚本 cpp 里写一行即可,Inspector 下拉与序列化按
|
||||||
|
// 类名(字符串)解析。T 须默认可构造并继承 ScriptBehavior
|
||||||
|
#define ENGINE_REGISTER_SCRIPT(T) \
|
||||||
|
static const bool s_##T##_registered = [] { \
|
||||||
|
::Engine::Scripting::ScriptRegistry::Register( \
|
||||||
|
#T, []() -> ::Engine::Scope< \
|
||||||
|
::Engine::Scripting::ScriptBehavior> { \
|
||||||
|
return ::Engine::CreateScope<T>(); \
|
||||||
|
}); \
|
||||||
|
return true; \
|
||||||
|
}();
|
||||||
|
|
||||||
|
#endif //HELLOVULKAN_SCRIPT_H
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// ScriptRuntime.h:脚本生命周期驱动。对账式管理(组件加/删/改名 ↔
|
||||||
|
// 实例建/毁),照 PhysicsWorld.SyncScene 模式——每帧对账,编辑器操作
|
||||||
|
// 即时生效,场景加载/实体销毁自动消化。
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef HELLOVULKAN_SCRIPT_RUNTIME_H
|
||||||
|
#define HELLOVULKAN_SCRIPT_RUNTIME_H
|
||||||
|
|
||||||
|
#include <Engine/Core/Types.h>
|
||||||
|
#include <Engine/ECS/Scene.h>
|
||||||
|
#include <Engine/Scripting/Script.h>
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace Engine {
|
||||||
|
|
||||||
|
class Application;
|
||||||
|
|
||||||
|
namespace Scripting {
|
||||||
|
|
||||||
|
// 实例簿记:行为对象 + OnCreate 是否已调
|
||||||
|
struct ScriptInstance {
|
||||||
|
Scope<ScriptBehavior> behavior;
|
||||||
|
bool created = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ScriptRuntime {
|
||||||
|
public:
|
||||||
|
explicit ScriptRuntime(ECS::Scene& scene, Application* app = nullptr)
|
||||||
|
: m_Scene(scene), m_App(app) {}
|
||||||
|
|
||||||
|
// 每帧调用(吃 scaled dt):对账 → OnCreate(新实例)→ OnUpdate
|
||||||
|
void Update(f32 dt);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Sync();
|
||||||
|
|
||||||
|
ECS::Scene& m_Scene;
|
||||||
|
Application* m_App; // 注入脚本上下文
|
||||||
|
std::unordered_map<u32, ScriptInstance> m_Instances; // entityBits
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Scripting
|
||||||
|
} // namespace Engine
|
||||||
|
|
||||||
|
#endif //HELLOVULKAN_SCRIPT_RUNTIME_H
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
//
|
||||||
|
// Created by atdunbg on 2026/10/3.
|
||||||
|
//
|
||||||
|
// SceneSerializer:场景 → YAML 文件的双向转换(Unity 场景 YAML 同构简化)。
|
||||||
|
//
|
||||||
|
// 参照系(定案):Unity 的场景 YAML 结构 + UE AssetRegistry 式中央
|
||||||
|
// GUID 注册表(AssetManager 维护,存 assets/asset_registry.yaml)。
|
||||||
|
// 场景内资产引用双写 {Guid, Path}:GUID 是主键(重命名/移动资产不断
|
||||||
|
// 引用),Path 是人工可读提示 + 注册表缺失时的解析兜底。
|
||||||
|
//
|
||||||
|
// 引擎层身份:本类不感知编辑器(kEnvPaths 表)与渲染器——
|
||||||
|
// environmentPath 以普通字符串经参数进出,消费方(编辑器/游戏运行时)
|
||||||
|
// 自行解释。一套 API 服务两个消费者:编辑器 File 菜单 与
|
||||||
|
// 游戏运行时动态加载,不存在"另一套序列化"。
|
||||||
|
//
|
||||||
|
// 前向兼容(Prefab 路线预留):
|
||||||
|
// - 场景 = 实体节点列表,Prefab 复用同一套实体读写函数
|
||||||
|
// - Version 闸:高版本文件拒载并 WARN,低版本宽容
|
||||||
|
// - 未知组件名跳过(Unity 语义:新版场景在旧版引擎打开可玩)
|
||||||
|
// - UUID 重复 → 重新生成(即 Prefab 实例化的 ID 重映射)
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef HELLOVULKAN_SCENESERIALIZER_H
|
||||||
|
#define HELLOVULKAN_SCENESERIALIZER_H
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Engine::ECS { class Scene; }
|
||||||
|
namespace Engine::Assets { class AssetManager; }
|
||||||
|
|
||||||
|
namespace Engine::Serialization {
|
||||||
|
|
||||||
|
// 当前场景文件格式版本(字段/结构变更时递增)
|
||||||
|
inline constexpr int kSceneVersion = 1;
|
||||||
|
|
||||||
|
class SceneSerializer {
|
||||||
|
public:
|
||||||
|
// ---- 保存 ----
|
||||||
|
// scene 序列化到 outPath(YAML)。environmentPath 可空(不写
|
||||||
|
// Environment 字段)。失败(无法写文件)返回 false。
|
||||||
|
static bool Serialize(const ECS::Scene& scene,
|
||||||
|
Assets::AssetManager& assets,
|
||||||
|
const std::string& outPath,
|
||||||
|
const std::string& environmentPath = {});
|
||||||
|
|
||||||
|
// ---- 加载 ----
|
||||||
|
// 原子换血:先解析校验到中间结构,全部成功才 scene.Clear() 重建
|
||||||
|
//(解析失败不动现场)。成功时 outEnvironmentPath 带回场景记录的
|
||||||
|
// 环境路径(空 = 场景未带环境,消费方保持当前环境即可)。
|
||||||
|
// 健壮性:单实体损坏跳过继续、缺字段取默认值、未知组件跳过、
|
||||||
|
// UUID 重复重新生成、YAML 异常优雅报错。
|
||||||
|
// 注意:调用方持有的 Entity/EntityHandle 在成功后全部悬空,
|
||||||
|
// 必须重解析(Scene::GetEntityHandle / 按 UUID 查询)。
|
||||||
|
static bool Deserialize(ECS::Scene& scene,
|
||||||
|
Assets::AssetManager& assets,
|
||||||
|
const std::string& inPath,
|
||||||
|
std::string& outEnvironmentPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Engine::Serialization
|
||||||
|
|
||||||
|
#endif //HELLOVULKAN_SCENESERIALIZER_H
|
||||||
@@ -4,13 +4,106 @@
|
|||||||
|
|
||||||
#include <Engine/Assets/AssetManager.h>
|
#include <Engine/Assets/AssetManager.h>
|
||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
|
#include <Engine/Core/UUID.h>
|
||||||
|
|
||||||
|
#include <yaml-cpp/yaml.h>
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
namespace Engine::Assets {
|
namespace Engine::Assets {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// GUID 注册表落盘位置(相对 CWD,与 assets/ 约定一致)
|
||||||
|
constexpr const char* kGuidRegistryPath = "assets/asset_registry.yaml";
|
||||||
|
// 伪路径前缀:参数确定性生成,无稳定身份需求,不进 GUID 注册表
|
||||||
|
constexpr std::string_view kProceduralPrefix = "procedural://";
|
||||||
|
bool IsProceduralPath(const std::string& path)
|
||||||
|
{
|
||||||
|
return path.rfind(kProceduralPrefix, 0) == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AssetManager::AssetManager(IAssetUploader& uploader)
|
AssetManager::AssetManager(IAssetUploader& uploader)
|
||||||
: m_Uploader(uploader)
|
: m_Uploader(uploader)
|
||||||
{
|
{
|
||||||
ENGINE_INFO("AssetManager created");
|
LoadGUIDRegistry();
|
||||||
|
ENGINE_INFO("AssetManager created ({} guids registered)", m_GuidToPath.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 资产 GUID 注册表 ----
|
||||||
|
|
||||||
|
u64 AssetManager::GetOrCreateGUID(const std::string& path)
|
||||||
|
{
|
||||||
|
if (path.empty() || IsProceduralPath(path)) return 0;
|
||||||
|
|
||||||
|
if (auto it = m_PathToGuid.find(path); it != m_PathToGuid.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 64 位随机 + 双 map 双向查重(UUID::New 撞已有 GUID 概率可忽略,
|
||||||
|
// 但注册表是持久身份,防御性重试)
|
||||||
|
u64 guid = 0;
|
||||||
|
do { guid = UUID::New().Value(); } while (m_GuidToPath.contains(guid));
|
||||||
|
|
||||||
|
m_PathToGuid.emplace(path, guid);
|
||||||
|
m_GuidToPath.emplace(guid, path);
|
||||||
|
SaveGUIDRegistry();
|
||||||
|
return guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AssetManager::ResolveGUID(const u64 guid) const
|
||||||
|
{
|
||||||
|
if (auto it = m_GuidToPath.find(guid); it != m_GuidToPath.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManager::LoadGUIDRegistry()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
YAML::Node root = YAML::LoadFile(kGuidRegistryPath);
|
||||||
|
if (const YAML::Node assets = root["Assets"]; assets.IsSequence()) {
|
||||||
|
for (const auto& a : assets) {
|
||||||
|
const u64 guid = a["Guid"].as<u64>(0);
|
||||||
|
auto path = a["Path"].as<std::string>();
|
||||||
|
if (guid != 0 && !path.empty() &&
|
||||||
|
!m_GuidToPath.contains(guid)) {
|
||||||
|
m_GuidToPath.emplace(guid, path);
|
||||||
|
m_PathToGuid.emplace(std::move(path), guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const YAML::Exception& e) {
|
||||||
|
// 注册表缺失/损坏不致命:场景里同时存了 Path 兜底
|
||||||
|
ENGINE_WARN("AssetManager: guid registry unavailable ({})", e.what());
|
||||||
|
}
|
||||||
|
catch (const std::exception& e) {
|
||||||
|
ENGINE_WARN("AssetManager: guid registry unreadable ({})", e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AssetManager::SaveGUIDRegistry() const
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
YAML::Node root;
|
||||||
|
for (const auto& [guid, path] : m_GuidToPath) {
|
||||||
|
YAML::Node a;
|
||||||
|
a["Guid"] = guid;
|
||||||
|
a["Path"] = path;
|
||||||
|
root["Assets"].push_back(a);
|
||||||
|
}
|
||||||
|
std::ofstream out(kGuidRegistryPath, std::ios::trunc);
|
||||||
|
if (!out) {
|
||||||
|
ENGINE_WARN("AssetManager: cannot write {}", kGuidRegistryPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out << root;
|
||||||
|
}
|
||||||
|
catch (const YAML::Exception& e) {
|
||||||
|
ENGINE_ERROR("AssetManager: guid registry save failed ({})", e.what());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ModelRef AssetManager::LoadModel(const std::string& path)
|
ModelRef AssetManager::LoadModel(const std::string& path)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <Engine/Core/Log.h>
|
#include <Engine/Core/Log.h>
|
||||||
#include <Engine/ECS/Scene.h>
|
#include <Engine/ECS/Scene.h>
|
||||||
#include <Engine/Physics/PhysicsWorld.h>
|
#include <Engine/Physics/PhysicsWorld.h>
|
||||||
|
#include <Engine/Scripting/ScriptRuntime.h>
|
||||||
#include <Engine/Renderer/Renderer.h>
|
#include <Engine/Renderer/Renderer.h>
|
||||||
#include <Engine/Assets/AssetManager.h>
|
#include <Engine/Assets/AssetManager.h>
|
||||||
#include <Engine/Vulkan/Device.h>
|
#include <Engine/Vulkan/Device.h>
|
||||||
@@ -102,6 +103,10 @@ namespace Engine {
|
|||||||
// 物理调试图元 → 渲染侧(PhysicsDebugPass 每帧上传绘制)
|
// 物理调试图元 → 渲染侧(PhysicsDebugPass 每帧上传绘制)
|
||||||
m_Renderer->SetPhysicsDebugSource(&m_Physics->GetDebugFrame());
|
m_Renderer->SetPhysicsDebugSource(&m_Physics->GetDebugFrame());
|
||||||
|
|
||||||
|
// 脚本生命周期(对账式,组件增删/改名即时生效);this 注入脚本
|
||||||
|
// 上下文(Window/Renderer/Physics 门面)
|
||||||
|
m_Scripts = CreateScope<Scripting::ScriptRuntime>(*m_Scene, this);
|
||||||
|
|
||||||
while (m_Running) {
|
while (m_Running) {
|
||||||
const auto frameStart = std::chrono::steady_clock::now();
|
const auto frameStart = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
@@ -121,6 +126,10 @@ namespace Engine {
|
|||||||
// SyncScene 对账仍跑(拖静态体照常同步)
|
// SyncScene 对账仍跑(拖静态体照常同步)
|
||||||
m_Physics->Step(frame.Dt());
|
m_Physics->Step(frame.Dt());
|
||||||
|
|
||||||
|
// 脚本更新(物理之后、业务 OnUpdate 之前,Unity FixedUpdate→Update
|
||||||
|
// 语义;吃 scaled dt——timeScale=0 暂停时 dt=0 脚本自然停摆)
|
||||||
|
m_Scripts->Update(frame.Dt());
|
||||||
|
|
||||||
OnUpdate(frame);
|
OnUpdate(frame);
|
||||||
|
|
||||||
// 延迟销毁在渲染前统一 flush,保证渲染遍历时池结构不变
|
// 延迟销毁在渲染前统一 flush,保证渲染遍历时池结构不变
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ namespace Engine
|
|||||||
if (!self->m_EventCallback) return;
|
if (!self->m_EventCallback) return;
|
||||||
if (self->m_MouseCapture && self->m_MouseCapture()) return;
|
if (self->m_MouseCapture && self->m_MouseCapture()) return;
|
||||||
|
|
||||||
|
self->m_ScrollAccum += static_cast<f32>(yoffset); // 轮询消费通道
|
||||||
|
|
||||||
Event e;
|
Event e;
|
||||||
e.type = EventType::MouseScroll;
|
e.type = EventType::MouseScroll;
|
||||||
e.deltaX = static_cast<f32>(xoffset);
|
e.deltaX = static_cast<f32>(xoffset);
|
||||||
@@ -245,4 +247,11 @@ namespace Engine
|
|||||||
dx = dy = 0.0f;
|
dx = dy = 0.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float Window::ConsumeScrollDelta()
|
||||||
|
{
|
||||||
|
const float d = m_ScrollAccum;
|
||||||
|
m_ScrollAccum = 0.0f;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
@@ -53,6 +53,19 @@ namespace Engine::ECS {
|
|||||||
m_PendingDestroy.clear();
|
m_PendingDestroy.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Scene::Clear()
|
||||||
|
{
|
||||||
|
// 先清延迟队列再销毁全部:避免 FlushDestroyQueue 语义混淆
|
||||||
|
//(registry.clear() 后队列里的句柄全部失效,直接丢弃即可)
|
||||||
|
m_PendingDestroy.clear();
|
||||||
|
m_Registry.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityHandle Scene::GetEntityHandle(Entity e)
|
||||||
|
{
|
||||||
|
return m_Registry.valid(e) ? EntityHandle{this, e} : EntityHandle{};
|
||||||
|
}
|
||||||
|
|
||||||
void Scene::UpdateTransforms()
|
void Scene::UpdateTransforms()
|
||||||
{
|
{
|
||||||
ZoneScoped;
|
ZoneScoped;
|
||||||
@@ -68,7 +81,7 @@ namespace Engine::ECS {
|
|||||||
CameraComponent* found = nullptr;
|
CameraComponent* found = nullptr;
|
||||||
Entity foundEntity = entt::null;
|
Entity foundEntity = entt::null;
|
||||||
m_Registry.view<CameraComponent>().each([&](Entity e, CameraComponent& cc) {
|
m_Registry.view<CameraComponent>().each([&](Entity e, CameraComponent& cc) {
|
||||||
if (!found && cc.primary) {
|
if (!found && cc.primary && cc.active) { // 禁用相机退出候选
|
||||||
found = &cc;
|
found = &cc;
|
||||||
foundEntity = e;
|
foundEntity = e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
//
|
||||||
|
// CharacterController.cpp:Jolt CharacterVirtual 封装实现。
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <Engine/Physics/CharacterController.h>
|
||||||
|
|
||||||
|
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
|
||||||
|
#include <Jolt/Physics/PhysicsSystem.h>
|
||||||
|
#include <Jolt/Physics/Body/Body.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/Shape.h>
|
||||||
|
#include <Jolt/Renderer/DebugRenderer.h>
|
||||||
|
|
||||||
|
namespace Engine {
|
||||||
|
|
||||||
|
CharacterController::CharacterController(JPH::PhysicsSystem &system,
|
||||||
|
const Params ¶ms,
|
||||||
|
const glm::vec3 &position)
|
||||||
|
: m_Params(params) {
|
||||||
|
// 胶囊体以脚底为原点:CapsuleShape 原点在其中心,经 mShapeOffset
|
||||||
|
// 上抬 (halfHeight + radius) 使底端落在原点(Jolt 角色约定)
|
||||||
|
JPH::CharacterVirtualSettings s;
|
||||||
|
s.mUp = JPH::Vec3::sAxisY();
|
||||||
|
s.mMass = std::max(params.mass, 1e-3f);
|
||||||
|
s.mMaxSlopeAngle = glm::radians(params.maxSlopeAngle);
|
||||||
|
s.mShape = new JPH::CapsuleShape(
|
||||||
|
std::max(params.radius, 0.01f),
|
||||||
|
std::max(params.halfHeight, 0.0f));
|
||||||
|
s.mShapeOffset = JPH::Vec3(0.0f,
|
||||||
|
params.halfHeight + params.radius, 0.0f);
|
||||||
|
|
||||||
|
m_Character = new JPH::CharacterVirtual(
|
||||||
|
&s, JPH::RVec3(position.x, position.y, position.z),
|
||||||
|
JPH::Quat::sIdentity(), &system);
|
||||||
|
|
||||||
|
// 台阶攀爬:竖直抬升 = stepHeight(ExtendedUpdate 内部走
|
||||||
|
// WalkStairs 前向距离按速度取)
|
||||||
|
m_Ext.mWalkStairsStepUp = JPH::Vec3(0.0f, std::max(params.stepHeight, 0.0f), 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterController::Update(const f32 dt, const JPH::Vec3 &gravity,
|
||||||
|
JPH::TempAllocator &allocator) {
|
||||||
|
// 重力积分:着地时清掉向下分量(防贴地累积穿透速度),悬空时累加
|
||||||
|
const JPH::Vec3 v = m_Character->GetLinearVelocity();
|
||||||
|
if (m_Character->IsSupported()) {
|
||||||
|
if (v.GetY() < 0.0f) {
|
||||||
|
m_Character->SetLinearVelocity(
|
||||||
|
JPH::Vec3(v.GetX(), 0.0f, v.GetZ()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m_Character->SetLinearVelocity(v + gravity * dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫掠推进(撞墙滑动 + 斜坡限制 + StickToFloor 贴地 + WalkStairs
|
||||||
|
// 上台阶)。过滤器默认全放行:角色与静/动层全碰
|
||||||
|
m_Character->ExtendedUpdate(dt, gravity, m_Ext,
|
||||||
|
JPH::BroadPhaseLayerFilter{},
|
||||||
|
JPH::ObjectLayerFilter{},
|
||||||
|
JPH::BodyFilter{}, JPH::ShapeFilter{},
|
||||||
|
allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterController::SetLinearVelocity(const glm::vec3 &velocity) {
|
||||||
|
m_Character->SetLinearVelocity(
|
||||||
|
JPH::Vec3(velocity.x, velocity.y, velocity.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 CharacterController::GetLinearVelocity() const {
|
||||||
|
const JPH::Vec3 v = m_Character->GetLinearVelocity();
|
||||||
|
return {v.GetX(), v.GetY(), v.GetZ()};
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterController::Jump(const f32 speed) {
|
||||||
|
const JPH::Vec3 v = m_Character->GetLinearVelocity();
|
||||||
|
m_Character->SetLinearVelocity(JPH::Vec3(v.GetX(), speed, v.GetZ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterController::IsGrounded() const {
|
||||||
|
return m_Character->IsSupported();
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 CharacterController::GetPosition() const {
|
||||||
|
const JPH::RVec3 p = m_Character->GetPosition();
|
||||||
|
return {static_cast<f32>(p.GetX()), static_cast<f32>(p.GetY()),
|
||||||
|
static_cast<f32>(p.GetZ())};
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterController::Teleport(const glm::vec3 &position) const {
|
||||||
|
m_Character->SetPosition(
|
||||||
|
JPH::RVec3(position.x, position.y, position.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterController::DebugDraw(JPH::DebugRenderer &renderer) const {
|
||||||
|
// 胶囊中心 = 脚底原点 + shapeOffset(角色不旋转,纯平移)。
|
||||||
|
// CharacterVirtual 非 body,DrawBodies 永远画不到它——这是
|
||||||
|
// Jolt 层面的缺口(连 CharacterVirtual::DebugDraw 都没有)
|
||||||
|
const JPH::RMat44 t = JPH::RMat44::sTranslation(
|
||||||
|
m_Character->GetPosition() +
|
||||||
|
JPH::RVec3(m_Character->GetShapeOffset()));
|
||||||
|
m_Character->GetShape()->Draw(&renderer, t, JPH::Vec3::sOne(),
|
||||||
|
JPH::Color::sGreen, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Engine
|
||||||
@@ -16,6 +16,12 @@
|
|||||||
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
|
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
|
#include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
|
||||||
|
#include <Jolt/Physics/Constraints/FixedConstraint.h>
|
||||||
|
#include <Jolt/Physics/Constraints/HingeConstraint.h>
|
||||||
|
#include <Jolt/Physics/Constraints/PointConstraint.h>
|
||||||
|
|
||||||
#include <Engine/Assets/AssetManager.h>
|
#include <Engine/Assets/AssetManager.h>
|
||||||
#include <Engine/Renderer/Model.h>
|
#include <Engine/Renderer/Model.h>
|
||||||
@@ -83,6 +89,26 @@ namespace Engine {
|
|||||||
glm::quat ToGlm(const JPH::Quat &q) {
|
glm::quat ToGlm(const JPH::Quat &q) {
|
||||||
return {q.GetW(), q.GetX(), q.GetY(), q.GetZ()};
|
return {q.GetW(), q.GetX(), q.GetY(), q.GetZ()};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 变更检测快照取值:collider 决定哪组参数生效(collider 本身变更
|
||||||
|
// 在外层先触发重建,到这里时语义已对齐)。Box/Mesh/Compound 不用
|
||||||
|
void ExtractShapeParams(const ECS::RigidBodyComponent &rb,
|
||||||
|
f32 &radius, f32 &halfHeight) {
|
||||||
|
using CT = ECS::RigidBodyComponent::ColliderType;
|
||||||
|
switch (rb.collider) {
|
||||||
|
case CT::Sphere: radius = rb.sphereRadius; break;
|
||||||
|
case CT::Capsule: radius = rb.capsuleRadius;
|
||||||
|
halfHeight = rb.capsuleHalfHeight; break;
|
||||||
|
case CT::Cylinder: radius = rb.cylinderRadius;
|
||||||
|
halfHeight = rb.cylinderHalfHeight; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 球/胶囊/圆柱类径向形状的缩放取 max 轴(不可各向异性)
|
||||||
|
f32 UniformScale(const glm::vec3 &s) {
|
||||||
|
return std::max({s.x, s.y, s.z});
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// ---- 碰撞事件监听:回调在 Update 内多线程并发,只读 body、加锁入队 ----
|
// ---- 碰撞事件监听:回调在 Update 内多线程并发,只读 body、加锁入队 ----
|
||||||
@@ -216,6 +242,7 @@ namespace Engine {
|
|||||||
|
|
||||||
m_System->Update(kFixedDt, steps, m_TempAllocator.get(), m_JobSystem.get());
|
m_System->Update(kFixedDt, steps, m_TempAllocator.get(), m_JobSystem.get());
|
||||||
Writeback();
|
Writeback();
|
||||||
|
UpdateCharacters(kFixedDt); // 角色扫掠(Jolt 官方建议:物理更新后)
|
||||||
UpdateAccels(kFixedDt); // 速度差分 → 加速度(step 率采样一次)
|
UpdateAccels(kFixedDt); // 速度差分 → 加速度(step 率采样一次)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +326,14 @@ namespace Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4) 角色控制器:CharacterVirtual 非 body,DrawBodies 画不到,
|
||||||
|
// 手动画胶囊线框(绿 = 与普通 collider 区分)
|
||||||
|
if ((m_DebugBits & kPhyDbgColliders) != 0) {
|
||||||
|
for (auto &[bits, entry] : m_Characters) {
|
||||||
|
entry.cc->DebugDraw(*m_DebugDraw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 释放上一帧缓存不再引用的通用几何 batch(DebugRenderer 内部池)
|
// 释放上一帧缓存不再引用的通用几何 batch(DebugRenderer 内部池)
|
||||||
m_DebugDraw->NextFrame();
|
m_DebugDraw->NextFrame();
|
||||||
}
|
}
|
||||||
@@ -308,6 +343,7 @@ namespace Engine {
|
|||||||
void PhysicsWorld::SyncScene() {
|
void PhysicsWorld::SyncScene() {
|
||||||
auto ® = m_Scene.GetRegistry();
|
auto ® = m_Scene.GetRegistry();
|
||||||
auto &bi = m_System->GetBodyInterface();
|
auto &bi = m_System->GetBodyInterface();
|
||||||
|
std::vector<u32> invalidated; // 本帧销毁/重建 body → 约束失效传染
|
||||||
|
|
||||||
// 1. 孤儿清理:实体销毁 / 组件被移除 → 移除 body
|
// 1. 孤儿清理:实体销毁 / 组件被移除 → 移除 body
|
||||||
for (auto it = m_Bodies.begin(); it != m_Bodies.end();) {
|
for (auto it = m_Bodies.begin(); it != m_Bodies.end();) {
|
||||||
@@ -316,6 +352,7 @@ namespace Engine {
|
|||||||
!reg.all_of<ECS::RigidBodyComponent, ECS::TransformComponent>(e)) {
|
!reg.all_of<ECS::RigidBodyComponent, ECS::TransformComponent>(e)) {
|
||||||
bi.RemoveBody(it->second.id);
|
bi.RemoveBody(it->second.id);
|
||||||
bi.DestroyBody(it->second.id);
|
bi.DestroyBody(it->second.id);
|
||||||
|
invalidated.push_back(it->first);
|
||||||
it = m_Bodies.erase(it);
|
it = m_Bodies.erase(it);
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
@@ -327,27 +364,46 @@ namespace Engine {
|
|||||||
for (auto [e, rb, tr]: view.each()) {
|
for (auto [e, rb, tr]: view.each()) {
|
||||||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||||
const auto found = m_Bodies.find(bits);
|
const auto found = m_Bodies.find(bits);
|
||||||
|
|
||||||
|
// 禁用 = 视为组件不存在:销毁 body(对账自然消化),重启用即重建
|
||||||
|
if (!rb.active) {
|
||||||
|
if (found != m_Bodies.end()) {
|
||||||
|
bi.RemoveBody(found->second.id);
|
||||||
|
bi.DestroyBody(found->second.id);
|
||||||
|
m_Bodies.erase(found);
|
||||||
|
invalidated.push_back(bits);
|
||||||
|
}
|
||||||
|
rb.bodyValid = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (found == m_Bodies.end() || !rb.bodyValid) {
|
if (found == m_Bodies.end() || !rb.bodyValid) {
|
||||||
if (found != m_Bodies.end()) {
|
if (found != m_Bodies.end()) {
|
||||||
bi.RemoveBody(found->second.id);
|
bi.RemoveBody(found->second.id);
|
||||||
bi.DestroyBody(found->second.id);
|
bi.DestroyBody(found->second.id);
|
||||||
m_Bodies.erase(found);
|
m_Bodies.erase(found);
|
||||||
|
invalidated.push_back(bits);
|
||||||
}
|
}
|
||||||
CreateBody(bits, rb, tr);
|
CreateBody(bits, rb, tr);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &rec = found->second; // 非 const:静态/运动体拖动时更新 lastPos/lastRot
|
auto &rec = found->second; // 非 const:静态/运动体拖动时更新 lastPos/lastRot
|
||||||
|
f32 radius = 0.0f, halfHeight = 0.0f;
|
||||||
|
ExtractShapeParams(rb, radius, halfHeight);
|
||||||
if (rec.type != static_cast<u8>(rb.type) ||
|
if (rec.type != static_cast<u8>(rb.type) ||
|
||||||
rec.collider != static_cast<u8>(rb.collider) ||
|
rec.collider != static_cast<u8>(rb.collider) ||
|
||||||
rec.colliderSize != rb.boxHalfExtents ||
|
rec.colliderSize != rb.boxHalfExtents ||
|
||||||
rec.radius != rb.sphereRadius ||
|
rec.radius != radius ||
|
||||||
|
rec.halfHeight != halfHeight ||
|
||||||
|
rec.compound != rb.compound ||
|
||||||
rec.mass != rb.mass ||
|
rec.mass != rb.mass ||
|
||||||
rec.isSensor != rb.isSensor) {
|
rec.isTrigger != rb.isTrigger) {
|
||||||
// 类型/碰撞体/质量变更 → 重建(简单正确,重置速度可接受)
|
// 类型/碰撞体/质量变更 → 重建(简单正确,重置速度可接受)
|
||||||
bi.RemoveBody(rec.id);
|
bi.RemoveBody(rec.id);
|
||||||
bi.DestroyBody(rec.id);
|
bi.DestroyBody(rec.id);
|
||||||
m_Bodies.erase(found);
|
m_Bodies.erase(found);
|
||||||
|
invalidated.push_back(bits);
|
||||||
rb.bodyValid = false;
|
rb.bodyValid = false;
|
||||||
CreateBody(bits, rb, tr);
|
CreateBody(bits, rb, tr);
|
||||||
continue;
|
continue;
|
||||||
@@ -358,6 +414,22 @@ namespace Engine {
|
|||||||
bi.SetRestitution(id, rb.restitution);
|
bi.SetRestitution(id, rb.restitution);
|
||||||
if (rb.type == ECS::RigidBodyComponent::BodyType::Dynamic) {
|
if (rb.type == ECS::RigidBodyComponent::BodyType::Dynamic) {
|
||||||
bi.SetGravityFactor(id, rb.gravityScale);
|
bi.SetGravityFactor(id, rb.gravityScale);
|
||||||
|
// 编辑器拖动 Dynamic 体(暂停期/物理未步进的帧)→ 传送 +
|
||||||
|
// 清速度(UE 编辑器语义:编辑器操作权威于物理 body,拖动 =
|
||||||
|
// 重新放置)。物理模拟中 Writeback 每步同步 lastPos/lastRot
|
||||||
|
// → 本比对只在真正拖动帧为真,不会打断自由模拟
|
||||||
|
if (rec.lastPos != tr.GetPosition() ||
|
||||||
|
rec.lastRot != tr.GetRotationQuat()) {
|
||||||
|
bi.SetPositionAndRotation(
|
||||||
|
id,
|
||||||
|
JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z),
|
||||||
|
ToJolt(tr.GetRotationQuat()),
|
||||||
|
JPH::EActivation::Activate);
|
||||||
|
bi.SetLinearVelocity(id, JPH::Vec3(0.0f, 0.0f, 0.0f));
|
||||||
|
bi.SetAngularVelocity(id, JPH::Vec3(0.0f, 0.0f, 0.0f));
|
||||||
|
rec.lastPos = tr.GetPosition();
|
||||||
|
rec.lastRot = tr.GetRotationQuat();
|
||||||
|
}
|
||||||
} else if (rec.lastPos != tr.GetPosition() ||
|
} else if (rec.lastPos != tr.GetPosition() ||
|
||||||
rec.lastRot != tr.GetRotationQuat()) {
|
rec.lastRot != tr.GetRotationQuat()) {
|
||||||
// 编辑器拖动静态/运动学体 → 传送 body
|
// 编辑器拖动静态/运动学体 → 传送 body
|
||||||
@@ -369,6 +441,241 @@ namespace Engine {
|
|||||||
rec.lastRot = tr.GetRotationQuat();
|
rec.lastRot = tr.GetRotationQuat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. 约束对账(body 循环之后:依赖新建 body 的最终 ID)
|
||||||
|
SyncConstraints(invalidated);
|
||||||
|
|
||||||
|
// 4. 角色对账(非刚体,不依赖 body 生命周期)
|
||||||
|
SyncCharacters();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 约束对账 ----
|
||||||
|
|
||||||
|
void PhysicsWorld::SyncConstraints(const std::vector<u32> &invalidated) {
|
||||||
|
auto ® = m_Scene.GetRegistry();
|
||||||
|
const auto tainted = [&invalidated](u32 bits) {
|
||||||
|
return std::find(invalidated.begin(), invalidated.end(), bits)
|
||||||
|
!= invalidated.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. owner 实体/组件没了(或被禁用),或任一端 body 本帧被销毁/重建 → 移除
|
||||||
|
for (auto it = m_Constraints.begin(); it != m_Constraints.end();) {
|
||||||
|
const u32 owner = it->first;
|
||||||
|
const auto ownerEnt = static_cast<entt::entity>(owner);
|
||||||
|
const ECS::JointComponent* jc =
|
||||||
|
reg.valid(ownerEnt) ? reg.try_get<ECS::JointComponent>(ownerEnt)
|
||||||
|
: nullptr;
|
||||||
|
const bool ownerGone = jc == nullptr || !jc->active;
|
||||||
|
if (ownerGone || tainted(owner) || tainted(it->second.targetBits)) {
|
||||||
|
m_System->RemoveConstraint(it->second.constraint.GetPtr());
|
||||||
|
it = m_Constraints.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 遍历组件:快照变更 → 重建;缺失 → 创建;禁用 → 跳过(上面已清)。
|
||||||
|
// target 惰性按 UUID 反查(UUID 失联/body 未就绪 → 下帧再试)
|
||||||
|
auto view = reg.view<ECS::JointComponent>();
|
||||||
|
for (auto [e, jc]: view.each()) {
|
||||||
|
if (!jc.active) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||||
|
const auto found = m_Constraints.find(bits);
|
||||||
|
if (found != m_Constraints.end()) {
|
||||||
|
if (found->second.snapshot == jc) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_System->RemoveConstraint(found->second.constraint.GetPtr());
|
||||||
|
m_Constraints.erase(found);
|
||||||
|
}
|
||||||
|
const ECS::Entity target = m_Scene.FindEntityByUUID(jc.target);
|
||||||
|
if (target == entt::null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const u32 targetBits = static_cast<u32>(entt::to_integral(target));
|
||||||
|
if (bits == targetBits || !m_Bodies.contains(bits) ||
|
||||||
|
!m_Bodies.contains(targetBits)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CreateConstraint(bits, targetBits, jc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhysicsWorld::CreateConstraint(u32 ownerBits, u32 targetBits,
|
||||||
|
const ECS::JointComponent &jc) {
|
||||||
|
auto ® = m_Scene.GetRegistry();
|
||||||
|
const auto &b1 = m_Bodies.find(ownerBits)->second;
|
||||||
|
const auto &b2 = m_Bodies.find(targetBits)->second;
|
||||||
|
|
||||||
|
// 锚点(Transform 局部系)× 本体缩放 → COM 局部系
|
||||||
|
//(EConstraintSpace::LocalToBodyCOM 要求减 Shape::GetCenterOfMass,
|
||||||
|
// Compound 等形状 COM 会偏离原点;Box/Sphere/Capsule 为零)
|
||||||
|
const auto *tr1 = reg.try_get<ECS::TransformComponent>(
|
||||||
|
static_cast<entt::entity>(ownerBits));
|
||||||
|
const auto *tr2 = reg.try_get<ECS::TransformComponent>(
|
||||||
|
static_cast<entt::entity>(targetBits));
|
||||||
|
if (tr1 == nullptr || tr2 == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const glm::vec3 s1 = tr1->GetScale();
|
||||||
|
const glm::vec3 s2 = tr2->GetScale();
|
||||||
|
|
||||||
|
// 双写锁读位姿/COM + 创建(SyncScene 在主线程,与 Update 不并发)
|
||||||
|
JPH::BodyLockWrite lock1(m_System->GetBodyLockInterface(), b1.id);
|
||||||
|
JPH::BodyLockWrite lock2(m_System->GetBodyLockInterface(), b2.id);
|
||||||
|
if (!lock1.Succeeded() || !lock2.Succeeded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JPH::Body &body1 = lock1.GetBody();
|
||||||
|
JPH::Body &body2 = lock2.GetBody();
|
||||||
|
const JPH::Quat r1 = body1.GetRotation();
|
||||||
|
const JPH::Quat r2 = body2.GetRotation();
|
||||||
|
const JPH::Vec3 com1 = body1.GetShape()->GetCenterOfMass();
|
||||||
|
const JPH::Vec3 com2 = body2.GetShape()->GetCenterOfMass();
|
||||||
|
const JPH::RVec3 p1(jc.anchorA.x * s1.x - com1.GetX(),
|
||||||
|
jc.anchorA.y * s1.y - com1.GetY(),
|
||||||
|
jc.anchorA.z * s1.z - com1.GetZ());
|
||||||
|
const JPH::RVec3 p2(jc.anchorB.x * s2.x - com2.GetX(),
|
||||||
|
jc.anchorB.y * s2.y - com2.GetY(),
|
||||||
|
jc.anchorB.z * s2.z - com2.GetZ());
|
||||||
|
|
||||||
|
JPH::Ref<JPH::Constraint> constraint;
|
||||||
|
if (jc.type == ECS::JointComponent::Type::Fixed) {
|
||||||
|
// 轴框架 = 同一世界姿态在各体局部系的表达 → 锁定创建时相对姿态
|
||||||
|
JPH::FixedConstraintSettings s;
|
||||||
|
s.mSpace = JPH::EConstraintSpace::LocalToBodyCOM;
|
||||||
|
s.mPoint1 = p1;
|
||||||
|
s.mPoint2 = p2;
|
||||||
|
s.mAxisX1 = r1.Conjugated() * JPH::Vec3::sAxisX();
|
||||||
|
s.mAxisY1 = r1.Conjugated() * JPH::Vec3::sAxisY();
|
||||||
|
s.mAxisX2 = r2.Conjugated() * JPH::Vec3::sAxisX();
|
||||||
|
s.mAxisY2 = r2.Conjugated() * JPH::Vec3::sAxisY();
|
||||||
|
constraint = s.Create(body1, body2);
|
||||||
|
} else if (jc.type == ECS::JointComponent::Type::Hinge) {
|
||||||
|
// 转轴:本体局部轴 → 世界(不吃缩放,归一化),再投到两体局部系;
|
||||||
|
// 法线任取与轴垂直的世界方向,保证两体框架共轴共面
|
||||||
|
JPH::HingeConstraintSettings s;
|
||||||
|
s.mSpace = JPH::EConstraintSpace::LocalToBodyCOM;
|
||||||
|
s.mPoint1 = p1;
|
||||||
|
s.mPoint2 = p2;
|
||||||
|
const JPH::Vec3 localAxis = JPH::Vec3(
|
||||||
|
jc.hingeAxis.x, jc.hingeAxis.y, jc.hingeAxis.z).Normalized();
|
||||||
|
const JPH::Vec3 worldAxis = r1 * localAxis;
|
||||||
|
JPH::Vec3 helper = std::abs(worldAxis.GetY()) < 0.99f
|
||||||
|
? JPH::Vec3::sAxisY()
|
||||||
|
: JPH::Vec3::sAxisX();
|
||||||
|
const JPH::Vec3 worldNormal = worldAxis.Cross(helper).Normalized();
|
||||||
|
s.mHingeAxis1 = r1.Conjugated() * worldAxis;
|
||||||
|
s.mNormalAxis1 = r1.Conjugated() * worldNormal;
|
||||||
|
s.mHingeAxis2 = r2.Conjugated() * worldAxis;
|
||||||
|
s.mNormalAxis2 = r2.Conjugated() * worldNormal;
|
||||||
|
if (jc.enableLimit) {
|
||||||
|
s.mLimitsMin = std::clamp(glm::radians(jc.limitMin),
|
||||||
|
-JPH::JPH_PI, 0.0f);
|
||||||
|
s.mLimitsMax = std::clamp(glm::radians(jc.limitMax),
|
||||||
|
0.0f, JPH::JPH_PI);
|
||||||
|
}
|
||||||
|
constraint = s.Create(body1, body2);
|
||||||
|
} else {
|
||||||
|
// Ball = PointConstraint(球窝:三旋转自由度全放开)
|
||||||
|
JPH::PointConstraintSettings s;
|
||||||
|
s.mSpace = JPH::EConstraintSpace::LocalToBodyCOM;
|
||||||
|
s.mPoint1 = p1;
|
||||||
|
s.mPoint2 = p2;
|
||||||
|
constraint = s.Create(body1, body2);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_System->AddConstraint(constraint.GetPtr());
|
||||||
|
|
||||||
|
ConstraintRecord rec;
|
||||||
|
rec.constraint = std::move(constraint);
|
||||||
|
rec.snapshot = jc;
|
||||||
|
rec.targetBits = targetBits;
|
||||||
|
m_Constraints[ownerBits] = rec;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhysicsWorld::RemoveConstraint(const u32 ownerBits) {
|
||||||
|
const auto found = m_Constraints.find(ownerBits);
|
||||||
|
if (found == m_Constraints.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_System->RemoveConstraint(found->second.constraint.GetPtr());
|
||||||
|
m_Constraints.erase(found); // Ref 析构 → 引用计数释放
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 角色控制器 ----
|
||||||
|
|
||||||
|
void PhysicsWorld::SyncCharacters() {
|
||||||
|
auto ® = m_Scene.GetRegistry();
|
||||||
|
|
||||||
|
// 1. 孤儿清理:实体销毁 / 组件被移除或禁用(Ref 析构即清理,无 body 负担)
|
||||||
|
for (auto it = m_Characters.begin(); it != m_Characters.end();) {
|
||||||
|
const auto e = static_cast<entt::entity>(it->first);
|
||||||
|
const auto* cc = reg.valid(e)
|
||||||
|
? reg.try_get<ECS::CharacterControllerComponent>(e) : nullptr;
|
||||||
|
if (cc == nullptr || !cc->active ||
|
||||||
|
!reg.all_of<ECS::TransformComponent>(e)) {
|
||||||
|
it = m_Characters.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 遍历:缺失 → 创建;参数变更 → 重建;编辑器拖动 → 传送;禁用 → 跳过
|
||||||
|
auto view = reg.view<ECS::CharacterControllerComponent,
|
||||||
|
ECS::TransformComponent>();
|
||||||
|
for (auto [e, cc, tr]: view.each()) {
|
||||||
|
if (!cc.active) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||||
|
CharacterController::Params p;
|
||||||
|
p.radius = cc.radius;
|
||||||
|
p.halfHeight = cc.capsuleHalfHeight;
|
||||||
|
p.mass = cc.mass;
|
||||||
|
p.maxSlopeAngle = cc.maxSlopeAngle;
|
||||||
|
p.stepHeight = cc.stepHeight;
|
||||||
|
|
||||||
|
auto found = m_Characters.find(bits);
|
||||||
|
if (found == m_Characters.end()) {
|
||||||
|
CharacterEntry entry;
|
||||||
|
entry.cc = std::make_unique<CharacterController>(
|
||||||
|
*m_System, p, tr.GetPosition());
|
||||||
|
entry.lastPos = tr.GetPosition();
|
||||||
|
m_Characters.emplace(bits, std::move(entry));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (found->second.cc->GetParams() != p) {
|
||||||
|
// 参数变更 → 重建(位置沿用当前 Transform)
|
||||||
|
found->second.cc = std::make_unique<CharacterController>(
|
||||||
|
*m_System, p, tr.GetPosition());
|
||||||
|
found->second.lastPos = tr.GetPosition();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (found->second.lastPos != tr.GetPosition()) {
|
||||||
|
// 编辑器拖动实体 → 传送角色(角色是唯一位置源,反向覆盖)
|
||||||
|
found->second.cc->Teleport(tr.GetPosition());
|
||||||
|
found->second.lastPos = tr.GetPosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhysicsWorld::UpdateCharacters(const f32 dt) {
|
||||||
|
auto ® = m_Scene.GetRegistry();
|
||||||
|
const JPH::Vec3 gravity = m_System->GetGravity();
|
||||||
|
for (auto &[bits, entry]: m_Characters) {
|
||||||
|
auto *tr = reg.try_get<ECS::TransformComponent>(
|
||||||
|
static_cast<entt::entity>(bits));
|
||||||
|
if (tr == nullptr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entry.cc->Update(dt, gravity, *m_TempAllocator);
|
||||||
|
const glm::vec3 p = entry.cc->GetPosition(); // 原点 = 脚底
|
||||||
|
tr->SetPosition(p); // 标脏 → UpdateTransforms 重算 world
|
||||||
|
entry.lastPos = p;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Mesh 碰撞体:视觉网格 → Jolt 形状 ----
|
// ---- Mesh 碰撞体:视觉网格 → Jolt 形状 ----
|
||||||
@@ -444,16 +751,80 @@ namespace Engine {
|
|||||||
return res.Get();
|
return res.Get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JPH::RefConst<JPH::Shape> PhysicsWorld::BuildCompoundShape(
|
||||||
|
ECS::RigidBodyComponent &rb, ECS::TransformComponent &tr) {
|
||||||
|
if (rb.compound.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩放烘焙:child 局部位置按轴缩放,尺寸按 max 轴(球/胶囊径向
|
||||||
|
// 不可各向异性)。rotation 存数据与序列化(首版 Inspector 不编辑)
|
||||||
|
const glm::vec3 s = tr.GetScale();
|
||||||
|
JPH::StaticCompoundShapeSettings cs;
|
||||||
|
for (const auto &child: rb.compound) {
|
||||||
|
JPH::RefConst<JPH::Shape> sub;
|
||||||
|
const f32 us = UniformScale(s);
|
||||||
|
switch (child.shape) {
|
||||||
|
case ECS::RigidBodyComponent::CompoundChild::Shape::Sphere:
|
||||||
|
sub = new JPH::SphereShape(
|
||||||
|
std::max(child.radius * us, 0.01f));
|
||||||
|
break;
|
||||||
|
case ECS::RigidBodyComponent::CompoundChild::Shape::Capsule:
|
||||||
|
sub = new JPH::CapsuleShape(
|
||||||
|
std::max(child.radius * us, 0.01f),
|
||||||
|
std::max(child.capsuleHalfHeight * us, 0.01f));
|
||||||
|
break;
|
||||||
|
default: // Box
|
||||||
|
sub = new JPH::BoxShape(JPH::Vec3(
|
||||||
|
std::max(child.boxHalfExtents.x * s.x, 0.01f),
|
||||||
|
std::max(child.boxHalfExtents.y * s.y, 0.01f),
|
||||||
|
std::max(child.boxHalfExtents.z * s.z, 0.01f)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cs.AddShape(JPH::Vec3(child.position.x * s.x,
|
||||||
|
child.position.y * s.y,
|
||||||
|
child.position.z * s.z),
|
||||||
|
ToJolt(child.rotation), sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
const JPH::Shape::ShapeResult res = cs.Create();
|
||||||
|
if (res.HasError()) {
|
||||||
|
ENGINE_WARN("[Physics] CompoundShape build failed: {}",
|
||||||
|
res.GetError().c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return res.Get();
|
||||||
|
}
|
||||||
|
|
||||||
void PhysicsWorld::CreateBody(u32 entityBits, ECS::RigidBodyComponent &rb,
|
void PhysicsWorld::CreateBody(u32 entityBits, ECS::RigidBodyComponent &rb,
|
||||||
ECS::TransformComponent &tr) {
|
ECS::TransformComponent &tr) {
|
||||||
auto &bi = m_System->GetBodyInterface();
|
auto &bi = m_System->GetBodyInterface();
|
||||||
|
|
||||||
// 碰撞体:组件显式尺寸 × Transform 缩放(形状按单位尺度烹饪 +
|
// 碰撞体:组件显式尺寸 × Transform 缩放(形状按单位尺度烹饪 +
|
||||||
// ScaledShape 的优化留给 ShapeCache 阶段;MVP 直接带缩放建形状)
|
// ScaledShape 的优化留给 ShapeCache 阶段;MVP 直接带缩放建形状)。
|
||||||
|
// 注意 Jolt 两 ctor 参数序相反:CapsuleShape(半径, 圆柱段半高)、
|
||||||
|
// CylinderShape(圆柱段半高, 半径)
|
||||||
JPH::RefConst<JPH::Shape> shape;
|
JPH::RefConst<JPH::Shape> shape;
|
||||||
|
const f32 uniformScale = UniformScale(tr.GetScale());
|
||||||
if (rb.collider == ECS::RigidBodyComponent::ColliderType::Sphere) {
|
if (rb.collider == ECS::RigidBodyComponent::ColliderType::Sphere) {
|
||||||
const f32 scale = std::max({tr.GetScale().x, tr.GetScale().y, tr.GetScale().z});
|
shape = new JPH::SphereShape(std::max(rb.sphereRadius * uniformScale, 0.01f));
|
||||||
shape = new JPH::SphereShape(std::max(rb.sphereRadius * scale, 0.01f));
|
} else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Capsule) {
|
||||||
|
shape = new JPH::CapsuleShape(
|
||||||
|
std::max(rb.capsuleRadius * uniformScale, 0.01f),
|
||||||
|
std::max(rb.capsuleHalfHeight * uniformScale, 0.01f));
|
||||||
|
} else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Cylinder) {
|
||||||
|
shape = new JPH::CylinderShape(
|
||||||
|
std::max(rb.cylinderHalfHeight * uniformScale, 0.01f),
|
||||||
|
std::max(rb.cylinderRadius * uniformScale, 0.01f));
|
||||||
|
} else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Compound) {
|
||||||
|
// 子形状列表 → StaticCompoundShape;空列表/构建失败回退 Box
|
||||||
|
shape = BuildCompoundShape(rb, tr);
|
||||||
|
if (shape == nullptr) {
|
||||||
|
shape = new JPH::BoxShape(JPH::Vec3(
|
||||||
|
std::max(rb.boxHalfExtents.x * tr.GetScale().x, 0.01f),
|
||||||
|
std::max(rb.boxHalfExtents.y * tr.GetScale().y, 0.01f),
|
||||||
|
std::max(rb.boxHalfExtents.z * tr.GetScale().z, 0.01f)));
|
||||||
|
}
|
||||||
} else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Mesh) {
|
} else if (rb.collider == ECS::RigidBodyComponent::ColliderType::Mesh) {
|
||||||
// Mesh 碰撞体:实体 MeshRenderer 的视觉网格(Unity MeshCollider
|
// Mesh 碰撞体:实体 MeshRenderer 的视觉网格(Unity MeshCollider
|
||||||
// 语义)。缩放烘进顶点(Jolt shape 无缩放);Static → MeshShape
|
// 语义)。缩放烘进顶点(Jolt shape 无缩放);Static → MeshShape
|
||||||
@@ -497,7 +868,7 @@ namespace Engine {
|
|||||||
settings.mFriction = rb.friction;
|
settings.mFriction = rb.friction;
|
||||||
settings.mRestitution = rb.restitution;
|
settings.mRestitution = rb.restitution;
|
||||||
settings.mUserData = entityBits; // 事件回调:Body → 实体位模式反查
|
settings.mUserData = entityBits; // 事件回调:Body → 实体位模式反查
|
||||||
settings.mIsSensor = rb.isSensor; // 传感器:只收 Added/Removed,不解算
|
settings.mIsSensor = rb.isTrigger; // 触发器:只收 Added/Removed,不解算
|
||||||
|
|
||||||
const JPH::BodyID id = bi.CreateAndAddBody(
|
const JPH::BodyID id = bi.CreateAndAddBody(
|
||||||
settings, dynamic ? JPH::EActivation::Activate : JPH::EActivation::DontActivate);
|
settings, dynamic ? JPH::EActivation::Activate : JPH::EActivation::DontActivate);
|
||||||
@@ -507,9 +878,10 @@ namespace Engine {
|
|||||||
rec.type = static_cast<u8>(rb.type);
|
rec.type = static_cast<u8>(rb.type);
|
||||||
rec.collider = static_cast<u8>(rb.collider);
|
rec.collider = static_cast<u8>(rb.collider);
|
||||||
rec.colliderSize = rb.boxHalfExtents;
|
rec.colliderSize = rb.boxHalfExtents;
|
||||||
rec.radius = rb.sphereRadius;
|
ExtractShapeParams(rb, rec.radius, rec.halfHeight);
|
||||||
|
rec.compound = rb.compound;
|
||||||
rec.mass = rb.mass;
|
rec.mass = rb.mass;
|
||||||
rec.isSensor = rb.isSensor;
|
rec.isTrigger = rb.isTrigger;
|
||||||
rec.lastPos = tr.GetPosition();
|
rec.lastPos = tr.GetPosition();
|
||||||
rec.lastRot = tr.GetRotationQuat();
|
rec.lastRot = tr.GetRotationQuat();
|
||||||
m_Bodies[entityBits] = rec;
|
m_Bodies[entityBits] = rec;
|
||||||
@@ -578,9 +950,97 @@ namespace Engine {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 刚体驱动(游戏逻辑/编辑器调试;见头文件语义注释)----
|
||||||
|
|
||||||
|
const PhysicsWorld::BodyRecord *PhysicsWorld::FindRecord(const u32 entityBits) const {
|
||||||
|
const auto found = m_Bodies.find(entityBits);
|
||||||
|
return found != m_Bodies.end() ? &found->second : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 力/扭矩/冲量:仅动态体(Jolt 对静态/运动学体施加力无意义)
|
||||||
|
void PhysicsWorld::AddForce(const u32 entityBits, const glm::vec3 &force) {
|
||||||
|
const auto *rec = FindRecord(entityBits);
|
||||||
|
if (!rec || rec->type != static_cast<u8>(ECS::RigidBodyComponent::BodyType::Dynamic))
|
||||||
|
return;
|
||||||
|
m_System->GetBodyInterface().AddForce(
|
||||||
|
rec->id, JPH::Vec3(force.x, force.y, force.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhysicsWorld::AddImpulse(const u32 entityBits, const glm::vec3 &impulse) {
|
||||||
|
const auto *rec = FindRecord(entityBits);
|
||||||
|
if (!rec || rec->type != static_cast<u8>(ECS::RigidBodyComponent::BodyType::Dynamic))
|
||||||
|
return;
|
||||||
|
m_System->GetBodyInterface().AddImpulse(
|
||||||
|
rec->id, JPH::Vec3(impulse.x, impulse.y, impulse.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhysicsWorld::AddTorque(const u32 entityBits, const glm::vec3 &torque) {
|
||||||
|
const auto *rec = FindRecord(entityBits);
|
||||||
|
if (!rec || rec->type != static_cast<u8>(ECS::RigidBodyComponent::BodyType::Dynamic))
|
||||||
|
return;
|
||||||
|
m_System->GetBodyInterface().AddTorque(
|
||||||
|
rec->id, JPH::Vec3(torque.x, torque.y, torque.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 速度直写:动态+运动学体都有效(运动学体速度即其运动方式)
|
||||||
|
void PhysicsWorld::SetLinearVelocity(const u32 entityBits, const glm::vec3 &velocity) {
|
||||||
|
const auto *rec = FindRecord(entityBits);
|
||||||
|
if (!rec || rec->type == static_cast<u8>(ECS::RigidBodyComponent::BodyType::Static))
|
||||||
|
return;
|
||||||
|
m_System->GetBodyInterface().SetLinearVelocity(
|
||||||
|
rec->id, JPH::Vec3(velocity.x, velocity.y, velocity.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhysicsWorld::SetAngularVelocity(const u32 entityBits, const glm::vec3 &angularVelocity) {
|
||||||
|
const auto *rec = FindRecord(entityBits);
|
||||||
|
if (!rec || rec->type == static_cast<u8>(ECS::RigidBodyComponent::BodyType::Static))
|
||||||
|
return;
|
||||||
|
m_System->GetBodyInterface().SetAngularVelocity(
|
||||||
|
rec->id, JPH::Vec3(angularVelocity.x, angularVelocity.y, angularVelocity.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 PhysicsWorld::GetLinearVelocity(const u32 entityBits) const {
|
||||||
|
const auto *rec = FindRecord(entityBits);
|
||||||
|
if (!rec || rec->type == static_cast<u8>(ECS::RigidBodyComponent::BodyType::Static))
|
||||||
|
return {0.0f, 0.0f, 0.0f};
|
||||||
|
const JPH::Vec3 v = m_System->GetBodyInterface().GetLinearVelocity(rec->id);
|
||||||
|
return {v.GetX(), v.GetY(), v.GetZ()};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 角色控制器门面 ----
|
||||||
|
|
||||||
|
CharacterController *PhysicsWorld::FindCharacter(const u32 entityBits) {
|
||||||
|
const auto found = m_Characters.find(entityBits);
|
||||||
|
return found != m_Characters.end() ? found->second.cc.get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhysicsWorld::SetCharacterVelocity(const u32 entityBits,
|
||||||
|
const glm::vec3 &velocity) {
|
||||||
|
if (CharacterController *cc = FindCharacter(entityBits)) {
|
||||||
|
cc->SetLinearVelocity(velocity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhysicsWorld::Jump(const u32 entityBits, const f32 speed) {
|
||||||
|
if (CharacterController *cc = FindCharacter(entityBits)) {
|
||||||
|
cc->Jump(speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PhysicsWorld::IsCharacterGrounded(const u32 entityBits) const {
|
||||||
|
const auto found = m_Characters.find(entityBits);
|
||||||
|
return found != m_Characters.end() && found->second.cc->IsGrounded();
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 PhysicsWorld::GetCharacterVelocity(const u32 entityBits) const {
|
||||||
|
const auto found = m_Characters.find(entityBits);
|
||||||
|
return found != m_Characters.end() ? found->second.cc->GetLinearVelocity()
|
||||||
|
: glm::vec3{0.0f};
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)----
|
// ---- 写回:动态体姿态 → Transform(标脏,帧循环统一 Compose)----
|
||||||
|
|
||||||
void PhysicsWorld::Writeback() const {
|
void PhysicsWorld::Writeback() {
|
||||||
auto ® = m_Scene.GetRegistry();
|
auto ® = m_Scene.GetRegistry();
|
||||||
const auto &bi = m_System->GetBodyInterface();
|
const auto &bi = m_System->GetBodyInterface();
|
||||||
|
|
||||||
@@ -598,6 +1058,14 @@ namespace Engine {
|
|||||||
|
|
||||||
const JPH::Vec3 vel = bi.GetLinearVelocity(id);
|
const JPH::Vec3 vel = bi.GetLinearVelocity(id);
|
||||||
rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ());
|
rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ());
|
||||||
|
|
||||||
|
// lastPos/lastRot 同步到 body 姿态:否则下帧 SyncScene 把物理
|
||||||
|
// 位移误判为编辑器拖动 → teleport 回旧位置死循环
|
||||||
|
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||||
|
if (auto it = m_Bodies.find(bits); it != m_Bodies.end()) {
|
||||||
|
it->second.lastPos = tr.GetPosition();
|
||||||
|
it->second.lastRot = tr.GetRotationQuat();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace Engine
|
} // namespace Engine
|
||||||
|
|||||||
+103
-19
@@ -8,16 +8,38 @@
|
|||||||
|
|
||||||
#include <glm/gtc/matrix_transform.hpp>
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
#include "GLFW/glfw3.h"
|
#include "GLFW/glfw3.h"
|
||||||
|
|
||||||
namespace Engine {
|
namespace Engine {
|
||||||
|
|
||||||
void Camera::Update(Window& window, ECS::TransformComponent& transform, const f32 deltaTime) const {
|
namespace {
|
||||||
// 鼠标视角(编辑器习惯):按住右键捕获光标进入自由视角,
|
// 欧拉(度)→ 旋转矩阵:应用序 Y→X,与 TransformComponent::
|
||||||
// 松开释放光标(ImGui 面板可正常交互)。m_MouseLookEnabled 总开关
|
// Compose 一致(roll 不参与 FPS/编辑器相机)
|
||||||
|
glm::mat3 EulerRotMat(const glm::vec3& rotDeg)
|
||||||
|
{
|
||||||
|
return static_cast<glm::mat3>(
|
||||||
|
glm::rotate(glm::mat4(1.0f), glm::radians(rotDeg.y),
|
||||||
|
glm::vec3(0, 1, 0)) *
|
||||||
|
glm::rotate(glm::mat4(1.0f), glm::radians(rotDeg.x),
|
||||||
|
glm::vec3(1, 0, 0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::Update(Window& window, ECS::TransformComponent& transform,
|
||||||
|
const f32 deltaTime, const bool flyRequiresRMB)
|
||||||
|
{
|
||||||
|
// ---- 输入模式(UE Scene view 语义):RMB=自由视角(鼠标转向
|
||||||
|
// + 飞行),MMB=焦点环绕(Shift+MMB=平移),滚轮=推拉。三种
|
||||||
|
// 光标型交互互斥共用捕获;编辑器模式 flyRequiresRMB=true 时
|
||||||
|
// 移动键仅 RMB 按住生效(否则与 Gizmo W/E/R 热键冲突)----
|
||||||
const bool wantLook = m_MouseLookEnabled &&
|
const bool wantLook = m_MouseLookEnabled &&
|
||||||
window.IsMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT);
|
window.IsMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT);
|
||||||
if (wantLook)
|
const bool wantOrbit = m_MouseLookEnabled &&
|
||||||
|
window.IsMouseButtonPressed(GLFW_MOUSE_BUTTON_MIDDLE);
|
||||||
|
|
||||||
|
if (wantLook || wantOrbit)
|
||||||
{
|
{
|
||||||
if (!window.IsCursorCaptured())
|
if (!window.IsCursorCaptured())
|
||||||
{
|
{
|
||||||
@@ -37,6 +59,42 @@ namespace Engine {
|
|||||||
rot.x = glm::clamp(
|
rot.x = glm::clamp(
|
||||||
rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f);
|
rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f);
|
||||||
transform.SetRotation(rot);
|
transform.SetRotation(rot);
|
||||||
|
|
||||||
|
if (wantOrbit)
|
||||||
|
{
|
||||||
|
if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT))
|
||||||
|
{
|
||||||
|
// ---- 屏幕平面平移:内容跟手(拖右 → 相机左移、
|
||||||
|
// 拖下 → 相机上移),pivot 同步平移保持环绕点的
|
||||||
|
// 屏幕相对位置 ----
|
||||||
|
const glm::mat3 r = EulerRotMat(rot);
|
||||||
|
const glm::vec3 delta =
|
||||||
|
(-r * glm::vec3(1, 0, 0)) * mouseDX +
|
||||||
|
(r * glm::vec3(0, 1, 0)) * mouseDY;
|
||||||
|
const f32 k = m_OrbitDistance * 0.002f; // m/px
|
||||||
|
transform.SetPosition(transform.GetPosition() + delta * k);
|
||||||
|
if (m_HasPivot) m_OrbitPivot += delta * k;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ---- 焦点环绕:转向后保持到 pivot 的距离。首次
|
||||||
|
// 环绕无 pivot:取当前视线上 m_OrbitDistance 处
|
||||||
|
if (!m_HasPivot)
|
||||||
|
{
|
||||||
|
m_OrbitPivot = transform.GetPosition() +
|
||||||
|
EulerRotMat(transform.GetRotation()) *
|
||||||
|
glm::vec3(0, 0, -1) *
|
||||||
|
m_OrbitDistance;
|
||||||
|
m_HasPivot = true;
|
||||||
|
}
|
||||||
|
m_OrbitDistance = glm::length(
|
||||||
|
transform.GetPosition() - m_OrbitPivot);
|
||||||
|
const glm::vec3 orbitFwd =
|
||||||
|
EulerRotMat(rot) * glm::vec3(0, 0, -1);
|
||||||
|
transform.SetPosition(m_OrbitPivot -
|
||||||
|
orbitFwd * m_OrbitDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (window.IsCursorCaptured())
|
else if (window.IsCursorCaptured())
|
||||||
@@ -44,24 +102,50 @@ namespace Engine {
|
|||||||
window.SetCursorCaptured(false);
|
window.SetCursorCaptured(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X,
|
// 基向量由(可能已更新的)实体旋转决定(本地 -Z 为视线方向)
|
||||||
// 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机)
|
const glm::mat3 r = EulerRotMat(transform.GetRotation());
|
||||||
const glm::vec3 rot = transform.GetRotation(); // 派生欧拉(度)
|
|
||||||
const glm::mat3 r = static_cast<glm::mat3>(
|
|
||||||
glm::rotate(glm::mat4(1.0f), glm::radians(rot.y),
|
|
||||||
glm::vec3(0, 1, 0)) *
|
|
||||||
glm::rotate(glm::mat4(1.0f), glm::radians(rot.x),
|
|
||||||
glm::vec3(1, 0, 0)));
|
|
||||||
const glm::vec3 forward = r * glm::vec3(0, 0, -1);
|
const glm::vec3 forward = r * glm::vec3(0, 0, -1);
|
||||||
const glm::vec3 right = r * glm::vec3(1, 0, 0);
|
const glm::vec3 right = r * glm::vec3(1, 0, 0);
|
||||||
|
|
||||||
const f32 speed = m_MoveSpeed * deltaTime;
|
// ---- 飞行移动:编辑器模式仅 RMB(wantLook)时生效;游戏模式
|
||||||
if (window.IsKeyPressed(GLFW_KEY_W)) { transform.SetPosition(transform.GetPosition() + forward * speed); }
|
// 常开(原行为)----
|
||||||
if (window.IsKeyPressed(GLFW_KEY_S)) { transform.SetPosition(transform.GetPosition() - forward * speed); }
|
if (wantLook || !flyRequiresRMB)
|
||||||
if (window.IsKeyPressed(GLFW_KEY_A)) { transform.SetPosition(transform.GetPosition() - right * speed); }
|
{
|
||||||
if (window.IsKeyPressed(GLFW_KEY_D)) { transform.SetPosition(transform.GetPosition() + right * speed); }
|
const f32 speed = m_MoveSpeed * deltaTime;
|
||||||
if (window.IsKeyPressed(GLFW_KEY_SPACE)) { transform.SetPosition(transform.GetPosition() + glm::vec3(0, speed, 0)); }
|
if (window.IsKeyPressed(GLFW_KEY_W)) { transform.SetPosition(transform.GetPosition() + forward * speed); }
|
||||||
if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT)) { transform.SetPosition(transform.GetPosition() - glm::vec3(0, speed, 0)); }
|
if (window.IsKeyPressed(GLFW_KEY_S)) { transform.SetPosition(transform.GetPosition() - forward * speed); }
|
||||||
|
if (window.IsKeyPressed(GLFW_KEY_A)) { transform.SetPosition(transform.GetPosition() - right * speed); }
|
||||||
|
if (window.IsKeyPressed(GLFW_KEY_D)) { transform.SetPosition(transform.GetPosition() + right * speed); }
|
||||||
|
if (window.IsKeyPressed(GLFW_KEY_SPACE)) { transform.SetPosition(transform.GetPosition() + glm::vec3(0, speed, 0)); }
|
||||||
|
if (window.IsKeyPressed(GLFW_KEY_LEFT_SHIFT)) { transform.SetPosition(transform.GetPosition() - glm::vec3(0, speed, 0)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 滚轮推拉:有 pivot 时改环绕距离(指数步进,近处精细/
|
||||||
|
// 远处大步);无 pivot 时沿视线平移 ----
|
||||||
|
const f32 wheel = window.ConsumeScrollDelta();
|
||||||
|
if (m_MouseLookEnabled && wheel != 0.0f)
|
||||||
|
{
|
||||||
|
if (m_HasPivot)
|
||||||
|
{
|
||||||
|
m_OrbitDistance = glm::clamp(
|
||||||
|
m_OrbitDistance * std::pow(0.9f, wheel), 0.5f, 500.0f);
|
||||||
|
transform.SetPosition(m_OrbitPivot - forward * m_OrbitDistance);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
transform.SetPosition(transform.GetPosition() +
|
||||||
|
forward * wheel * m_MoveSpeed * deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera::FocusOn(ECS::TransformComponent& transform, const glm::vec3& point)
|
||||||
|
{
|
||||||
|
const glm::vec3 fwd =
|
||||||
|
EulerRotMat(transform.GetRotation()) * glm::vec3(0, 0, -1);
|
||||||
|
m_OrbitPivot = point;
|
||||||
|
m_HasPivot = true;
|
||||||
|
transform.SetPosition(point - fwd * m_OrbitDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
glm::mat4 Camera::GetView(const ECS::TransformComponent& t) const
|
glm::mat4 Camera::GetView(const ECS::TransformComponent& t) const
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ namespace Engine
|
|||||||
Mesh::Mesh(Device& device, MemoryAllocator& allocator,
|
Mesh::Mesh(Device& device, MemoryAllocator& allocator,
|
||||||
const std::vector<Vertex>& vertices,
|
const std::vector<Vertex>& vertices,
|
||||||
const std::vector<std::vector<u32>>& lods,
|
const std::vector<std::vector<u32>>& lods,
|
||||||
Material* material)
|
Material* material,
|
||||||
|
const std::vector<glm::vec3>* cpuPositions)
|
||||||
: m_Material(material)
|
: m_Material(material)
|
||||||
, m_Slot(s_nextMeshSlot.fetch_add(1, std::memory_order_relaxed))
|
, m_Slot(s_nextMeshSlot.fetch_add(1, std::memory_order_relaxed))
|
||||||
{
|
{
|
||||||
@@ -35,11 +36,19 @@ namespace Engine
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 位置取数:蒙皮 mesh 用绑定姿态渲染空间副本(与视觉同源),
|
||||||
|
// 否则用原始顶点位置
|
||||||
|
const bool useBind = cpuPositions != nullptr &&
|
||||||
|
cpuPositions->size() == vertices.size();
|
||||||
|
auto posAt = [&](size_t i) -> const glm::vec3& {
|
||||||
|
return useBind ? (*cpuPositions)[i] : vertices[i].position;
|
||||||
|
};
|
||||||
|
|
||||||
// CPU 位置副本:物理 MeshShape/ConvexHull 数据源(GPU 上传后
|
// CPU 位置副本:物理 MeshShape/ConvexHull 数据源(GPU 上传后
|
||||||
// 完整 Vertex 不再保留,见 Mesh.h)
|
// 完整 Vertex 不再保留,见 Mesh.h)
|
||||||
m_CpuPositions.reserve(vertices.size());
|
m_CpuPositions.reserve(vertices.size());
|
||||||
for (const auto& v : vertices) {
|
for (size_t i = 0; i < vertices.size(); ++i) {
|
||||||
m_CpuPositions.push_back(v.position);
|
m_CpuPositions.push_back(posAt(i));
|
||||||
}
|
}
|
||||||
m_CpuIndices = lods[0];
|
m_CpuIndices = lods[0];
|
||||||
|
|
||||||
@@ -78,18 +87,19 @@ namespace Engine
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 局部空间包围球:顶点包围盒 → 中心 + 最大半径(视锥剔除用)
|
// 局部空间包围球:顶点包围盒 → 中心 + 最大半径(视锥剔除用)
|
||||||
|
// 蒙皮 mesh 用绑定姿态副本(原始空间包围球大一个数量级 = 剔除/拾取失真)
|
||||||
{
|
{
|
||||||
glm::vec3 mn(std::numeric_limits<f32>::max());
|
glm::vec3 mn(std::numeric_limits<f32>::max());
|
||||||
glm::vec3 mx(std::numeric_limits<f32>::lowest());
|
glm::vec3 mx(std::numeric_limits<f32>::lowest());
|
||||||
for (const auto& v : vertices) {
|
for (size_t i = 0; i < vertices.size(); ++i) {
|
||||||
mn = glm::min(mn, v.position);
|
mn = glm::min(mn, posAt(i));
|
||||||
mx = glm::max(mx, v.position);
|
mx = glm::max(mx, posAt(i));
|
||||||
}
|
}
|
||||||
m_Bounds.center = (mn + mx) * 0.5f;
|
m_Bounds.center = (mn + mx) * 0.5f;
|
||||||
m_Bounds.radius = 0.0f;
|
m_Bounds.radius = 0.0f;
|
||||||
for (const auto& v : vertices) {
|
for (size_t i = 0; i < vertices.size(); ++i) {
|
||||||
m_Bounds.radius = glm::max(m_Bounds.radius,
|
m_Bounds.radius = glm::max(m_Bounds.radius,
|
||||||
glm::length(v.position - m_Bounds.center));
|
glm::length(posAt(i) - m_Bounds.center));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,42 @@ ParsedModel Model::ParseFromDisk(const std::string& path)
|
|||||||
bone.name);
|
bone.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 蒙皮 mesh 的 CPU 位置副本换算到渲染空间:渲染 = palette(t)×raw
|
||||||
|
//(palette = global(bone)×invBind,与 shader 同式),物理/包围球取
|
||||||
|
// 绑定姿态。FBX 的单位换算折在骨骼链上(bind palette ≠ I),直接
|
||||||
|
// 用 raw 会让物理凸包大一个数量级(M1911 实测 10×)
|
||||||
|
if (!out.bones.empty()) {
|
||||||
|
std::vector<glm::mat4> globals(out.nodes.size());
|
||||||
|
for (size_t i = 0; i < out.nodes.size(); ++i) {
|
||||||
|
globals[i] = out.nodes[i].parent < 0
|
||||||
|
? out.nodes[i].localTransform
|
||||||
|
: globals[out.nodes[i].parent] * out.nodes[i].localTransform;
|
||||||
|
}
|
||||||
|
std::vector<glm::mat4> bindPalette(out.bones.size());
|
||||||
|
for (size_t b = 0; b < out.bones.size(); ++b) {
|
||||||
|
bindPalette[b] = globals[out.bones[b].nodeIdx]
|
||||||
|
* out.bones[b].invBind;
|
||||||
|
}
|
||||||
|
for (auto& pm : out.meshes) {
|
||||||
|
if (!pm.skinned) continue;
|
||||||
|
pm.cpuPositions.resize(pm.vertices.size());
|
||||||
|
for (size_t v = 0; v < pm.vertices.size(); ++v) {
|
||||||
|
const Vertex& vert = pm.vertices[v];
|
||||||
|
glm::vec3 p(0.0f);
|
||||||
|
f32 wsum = 0.0f;
|
||||||
|
for (int k = 0; k < 4; ++k) {
|
||||||
|
const f32 w = vert.weights[k];
|
||||||
|
if (w <= 0.0f) continue;
|
||||||
|
p += w * glm::vec3(bindPalette[vert.boneIds[k]]
|
||||||
|
* glm::vec4(vert.position, 1.0f));
|
||||||
|
wsum += w;
|
||||||
|
}
|
||||||
|
// 零权重顶点(shader 侧塌到原点)物理侧保留原始位置兜底
|
||||||
|
pm.cpuPositions[v] = wsum > 1e-6f ? p : vert.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ENGINE_INFO("Model parsed: {} ({} meshes, {} bones, {} anims)",
|
ENGINE_INFO("Model parsed: {} ({} meshes, {} bones, {} anims)",
|
||||||
@@ -383,7 +419,8 @@ Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
|
|||||||
for (auto& pm : parsed.meshes) {
|
for (auto& pm : parsed.meshes) {
|
||||||
Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
||||||
model->m_Meshes.push_back(CreateScope<Mesh>(
|
model->m_Meshes.push_back(CreateScope<Mesh>(
|
||||||
device, allocator, pm.vertices, pm.lods, mat));
|
device, allocator, pm.vertices, pm.lods, mat,
|
||||||
|
pm.cpuPositions.empty() ? nullptr : &pm.cpuPositions));
|
||||||
}
|
}
|
||||||
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
||||||
parsed.path, model->m_Meshes.size(), model->m_TextureCache.size());
|
parsed.path, model->m_Meshes.size(), model->m_TextureCache.size());
|
||||||
@@ -406,7 +443,8 @@ void Model::LoadModel(const std::string& path)
|
|||||||
for (auto& pm : parsed.meshes) {
|
for (auto& pm : parsed.meshes) {
|
||||||
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
||||||
m_Meshes.push_back(CreateScope<Mesh>(
|
m_Meshes.push_back(CreateScope<Mesh>(
|
||||||
m_Device, m_Allocator, pm.vertices, pm.lods, mat));
|
m_Device, m_Allocator, pm.vertices, pm.lods, mat,
|
||||||
|
pm.cpuPositions.empty() ? nullptr : &pm.cpuPositions));
|
||||||
}
|
}
|
||||||
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
|
||||||
path, m_Meshes.size(), m_TextureCache.size());
|
path, m_Meshes.size(), m_TextureCache.size());
|
||||||
@@ -577,6 +615,7 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
|||||||
|
|
||||||
// 骨骼权重灌顶点(无骨骼 mesh 顶点保持默认 idx0/w1 → identity 直通)
|
// 骨骼权重灌顶点(无骨骼 mesh 顶点保持默认 idx0/w1 → identity 直通)
|
||||||
CollectMeshBones(mesh, vertices, bones, boneSlots);
|
CollectMeshBones(mesh, vertices, bones, boneSlots);
|
||||||
|
out.skinned = hasBones;
|
||||||
|
|
||||||
for (u32 i = 0; i < mesh->mNumFaces; ++i) {
|
for (u32 i = 0; i < mesh->mNumFaces; ++i) {
|
||||||
aiFace face = mesh->mFaces[i];
|
aiFace face = mesh->mFaces[i];
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <Engine/Renderer/Renderer.h>
|
#include <Engine/Renderer/Renderer.h>
|
||||||
#include <Engine/Renderer/SceneRenderer.h>
|
#include <Engine/Renderer/SceneRenderer.h>
|
||||||
|
#include <Engine/Renderer/EditorCamera.h>
|
||||||
#include <Engine/Renderer/VulkanAssetUploader.h>
|
#include <Engine/Renderer/VulkanAssetUploader.h>
|
||||||
#include <Engine/Renderer/Passes/SceneGpu.h>
|
#include <Engine/Renderer/Passes/SceneGpu.h>
|
||||||
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
||||||
@@ -233,6 +234,11 @@ namespace Engine
|
|||||||
m_SceneRenderer->SetDisplayMode(mode);
|
m_SceneRenderer->SetDisplayMode(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EditorCamera& Renderer::GetEditorCamera()
|
||||||
|
{
|
||||||
|
return m_SceneRenderer->GetEditorCamera();
|
||||||
|
}
|
||||||
|
|
||||||
const FrameSceneData &Renderer::GetFrameSceneData() const
|
const FrameSceneData &Renderer::GetFrameSceneData() const
|
||||||
{
|
{
|
||||||
return m_SceneRenderer->Frame();
|
return m_SceneRenderer->Frame();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <Engine/ECS/Scene.h>
|
#include <Engine/ECS/Scene.h>
|
||||||
#include <Engine/ECS/Components.h>
|
#include <Engine/ECS/Components.h>
|
||||||
#include <Engine/Renderer/Camera.h>
|
#include <Engine/Renderer/Camera.h>
|
||||||
|
#include <Engine/Renderer/EditorCamera.h>
|
||||||
#include <Engine/Renderer/Frustum.h>
|
#include <Engine/Renderer/Frustum.h>
|
||||||
#include <Engine/Renderer/Model.h>
|
#include <Engine/Renderer/Model.h>
|
||||||
#include <Engine/Renderer/Mesh.h>
|
#include <Engine/Renderer/Mesh.h>
|
||||||
@@ -258,14 +259,21 @@ namespace Engine
|
|||||||
ubo.prevView = m_LastView;
|
ubo.prevView = m_LastView;
|
||||||
ubo.prevProj = m_LastProjNoJitter;
|
ubo.prevProj = m_LastProjNoJitter;
|
||||||
|
|
||||||
// ---- 1. 相机(位姿单一事实源 = 相机实体的 Transform)----
|
// ---- 1. 相机:渲染源按显示模式分派(UE 语义)。编辑器形态
|
||||||
TransformComponent* camTransform = nullptr;
|
// Scene view 恒用编辑器相机(视口 UI 状态,空场景可用);
|
||||||
auto* cameraComp = scene.GetPrimaryCamera(&camTransform);
|
// 游戏形态用场景 primary 相机实体(游戏逻辑所有)----
|
||||||
if (!cameraComp || !camTransform) {
|
ECS::TransformComponent* camTransform = nullptr;
|
||||||
// 无相机:本帧跳过(正常场景必有相机,安全网)
|
const Camera* camera = nullptr;
|
||||||
|
if (m_DisplayMode == kDisplayViewport) {
|
||||||
|
camTransform = &m_EditorCamera.GetTransform();
|
||||||
|
camera = &m_EditorCamera.GetController();
|
||||||
|
} else if (auto* cameraComp = scene.GetPrimaryCamera(&camTransform)) {
|
||||||
|
camera = &cameraComp->camera;
|
||||||
|
}
|
||||||
|
if (!camera || !camTransform) {
|
||||||
|
// 游戏形态无 primary 相机:本帧跳过(安全网)
|
||||||
return false; // 拾取请求未消费,留待下一帧重试
|
return false; // 拾取请求未消费,留待下一帧重试
|
||||||
}
|
}
|
||||||
const Camera& camera = cameraComp->camera;
|
|
||||||
|
|
||||||
// 拾取请求落帧(一次性消费:本帧 HitProxy 渲染 + 像素拷贝)。
|
// 拾取请求落帧(一次性消费:本帧 HitProxy 渲染 + 像素拷贝)。
|
||||||
// 放在相机校验之后:跳帧时请求保留,不丢点击
|
// 放在相机校验之后:跳帧时请求保留,不丢点击
|
||||||
@@ -276,8 +284,8 @@ namespace Engine
|
|||||||
|
|
||||||
const f32 aspect = static_cast<f32>(targetExtent.width) /
|
const f32 aspect = static_cast<f32>(targetExtent.width) /
|
||||||
static_cast<f32>(targetExtent.height);
|
static_cast<f32>(targetExtent.height);
|
||||||
ubo.view = camera.GetView(*camTransform);
|
ubo.view = camera->GetView(*camTransform);
|
||||||
ubo.proj = camera.GetProjection(aspect, 0.1f, 100.0f);
|
ubo.proj = camera->GetProjection(aspect, 0.1f, 100.0f);
|
||||||
|
|
||||||
// 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity)
|
// 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity)
|
||||||
ubo.projNoJitter = ubo.proj;
|
ubo.projNoJitter = ubo.proj;
|
||||||
@@ -320,6 +328,7 @@ namespace Engine
|
|||||||
u32 collectedCount = 0;
|
u32 collectedCount = 0;
|
||||||
scene.Each<TransformComponent, LightComponent>(
|
scene.Each<TransformComponent, LightComponent>(
|
||||||
[&](Entity, TransformComponent& t, LightComponent& light) {
|
[&](Entity, TransformComponent& t, LightComponent& light) {
|
||||||
|
if (!light.active) return; // 组件禁用 = 不参与光照
|
||||||
if (collectedCount >= kMaxLights) return;
|
if (collectedCount >= kMaxLights) return;
|
||||||
|
|
||||||
auto& dst = collected[collectedCount++];
|
auto& dst = collected[collectedCount++];
|
||||||
@@ -384,6 +393,7 @@ namespace Engine
|
|||||||
m_EntitySkinBase.clear();
|
m_EntitySkinBase.clear();
|
||||||
scene.Each<MeshRendererComponent, AnimationComponent>(
|
scene.Each<MeshRendererComponent, AnimationComponent>(
|
||||||
[&](Entity entity, MeshRendererComponent& mr, AnimationComponent& anim) {
|
[&](Entity entity, MeshRendererComponent& mr, AnimationComponent& anim) {
|
||||||
|
if (!anim.active) return; // 组件禁用 = 暂停采样(播放头不动)
|
||||||
const Model* model = assets.GetModel(mr.model);
|
const Model* model = assets.GetModel(mr.model);
|
||||||
if (!model || !model->HasAnimations()) return;
|
if (!model || !model->HasAnimations()) return;
|
||||||
const auto& clips = model->GetClips();
|
const auto& clips = model->GetClips();
|
||||||
@@ -505,6 +515,7 @@ namespace Engine
|
|||||||
|
|
||||||
scene.Each<TransformComponent, MeshRendererComponent>(
|
scene.Each<TransformComponent, MeshRendererComponent>(
|
||||||
[&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) {
|
[&](Entity entity, TransformComponent& t, MeshRendererComponent& mr) {
|
||||||
|
if (!mr.active) return; // 组件禁用 = 不渲染不拾取
|
||||||
Model* model = assets.GetModel(mr.model);
|
Model* model = assets.GetModel(mr.model);
|
||||||
if (!model) return;
|
if (!model) return;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
//
|
||||||
|
// ScriptRuntime.cpp:脚本对账与生命周期驱动。
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <Engine/Scripting/ScriptRuntime.h>
|
||||||
|
|
||||||
|
#include <Engine/Core/Log.h>
|
||||||
|
#include <Engine/ECS/Components.h>
|
||||||
|
|
||||||
|
namespace Engine {
|
||||||
|
namespace Scripting {
|
||||||
|
|
||||||
|
// 宿主门面访问器:Application 完整类型只在脚本 cpp 里需要,
|
||||||
|
// 这里仅回引成员(避免引擎头循环依赖)
|
||||||
|
Application& ScriptBehavior::App() const {
|
||||||
|
return *m_App;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptRuntime::Update(f32 dt) {
|
||||||
|
Sync();
|
||||||
|
|
||||||
|
auto& reg = m_Scene.GetRegistry();
|
||||||
|
for (auto& [bits, inst] : m_Instances) {
|
||||||
|
const auto e = static_cast<entt::entity>(bits);
|
||||||
|
const ECS::ScriptComponent* sc = reg.valid(e)
|
||||||
|
? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
|
||||||
|
if (!sc) {
|
||||||
|
continue; // 延迟销毁队列里,下帧 Sync 清理
|
||||||
|
}
|
||||||
|
if (!sc->active) {
|
||||||
|
continue; // 禁用 = 保留实例停 tick(Unity disable 语义)
|
||||||
|
}
|
||||||
|
if (!inst.created) {
|
||||||
|
inst.behavior->OnCreate();
|
||||||
|
inst.created = true;
|
||||||
|
}
|
||||||
|
inst.behavior->OnUpdate(dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptRuntime::Sync() {
|
||||||
|
auto& reg = m_Scene.GetRegistry();
|
||||||
|
|
||||||
|
// 1. 孤儿/改名清理:实体或组件没了、脚本名变了 → OnDestroy + 删除
|
||||||
|
for (auto it = m_Instances.begin(); it != m_Instances.end();) {
|
||||||
|
const auto e = static_cast<entt::entity>(it->first);
|
||||||
|
const ECS::ScriptComponent* sc =
|
||||||
|
reg.valid(e) ? reg.try_get<ECS::ScriptComponent>(e) : nullptr;
|
||||||
|
const bool stale = !sc || !it->second.behavior ||
|
||||||
|
it->second.behavior->Name() != sc->scriptName;
|
||||||
|
if (stale) {
|
||||||
|
if (it->second.behavior) {
|
||||||
|
it->second.behavior->OnDestroy();
|
||||||
|
}
|
||||||
|
it = m_Instances.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 缺失实例化:按名查注册表(未知名警告——场景含未注册脚本时
|
||||||
|
// 弱前向兼容,跳过不影响其余实体)
|
||||||
|
const auto view = reg.view<ECS::ScriptComponent>();
|
||||||
|
for (auto [e, sc] : view.each()) {
|
||||||
|
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||||
|
if (m_Instances.contains(bits) || sc.scriptName.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto behavior = ScriptRegistry::Create(sc.scriptName);
|
||||||
|
if (!behavior) {
|
||||||
|
ENGINE_WARN("Script '{}' not registered (entity {})",
|
||||||
|
sc.scriptName, bits);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
behavior->Bind(m_Scene, e, m_App);
|
||||||
|
|
||||||
|
ScriptInstance inst;
|
||||||
|
inst.behavior = std::move(behavior);
|
||||||
|
inst.behavior->OnCreate(); // Unity Awake 语义:实例化即回调
|
||||||
|
inst.created = true;
|
||||||
|
m_Instances.emplace(bits, std::move(inst));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Scripting
|
||||||
|
} // namespace Engine
|
||||||
@@ -0,0 +1,675 @@
|
|||||||
|
//
|
||||||
|
// Created by atdunbg on 2026/10/3.
|
||||||
|
//
|
||||||
|
// SceneSerializer:场景 ↔ YAML。结构见头文件注释。
|
||||||
|
//
|
||||||
|
// 写端:手动 Emitter 逐键输出(短集合用 Flow 单行数组/内联 map,
|
||||||
|
// 浮点 9 位精度保证 float32 无损往返)。
|
||||||
|
// 读端:yaml-cpp 新版对"僵尸节点"(缺失键的返回值)硬化——operator=、
|
||||||
|
// size()、Type()、链式 operator[] 一律抛 InvalidNode,只有 IsDefined/
|
||||||
|
// as<T>(fallback)/begin() 安全。因此读侧铁律:每层节点先
|
||||||
|
// `const Node n = parent["Key"]` 拷贝初始化 + `if (n)` 判空再访问,
|
||||||
|
// 绝不对可能缺失的键做赋值或链式下标。
|
||||||
|
//
|
||||||
|
// 原子性:Deserialize 先解析到中间结构(逐实体容错),全部成功才
|
||||||
|
// scene.Clear() 换血重建——解析失败不动现场。
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <Engine/Serialization/SceneSerializer.h>
|
||||||
|
#include <Engine/ECS/Scene.h>
|
||||||
|
#include <Engine/ECS/EntityHandle.h>
|
||||||
|
#include <Engine/ECS/Components.h>
|
||||||
|
#include <Engine/Assets/AssetManager.h>
|
||||||
|
#include <Engine/Core/Log.h>
|
||||||
|
#include <Engine/Core/UUID.h>
|
||||||
|
|
||||||
|
#include <yaml-cpp/yaml.h>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Engine::Serialization {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// ---- 写端辅助:Flow 单行数组(Position: [0, 1.2, -6])----
|
||||||
|
|
||||||
|
void EmitVec3(YAML::Emitter& e, const glm::vec3& v)
|
||||||
|
{
|
||||||
|
e << YAML::Flow << YAML::BeginSeq << v.x << v.y << v.z
|
||||||
|
<< YAML::EndSeq;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitVec4(YAML::Emitter& e, const glm::vec4& v)
|
||||||
|
{
|
||||||
|
e << YAML::Flow << YAML::BeginSeq << v.x << v.y << v.z << v.w
|
||||||
|
<< YAML::EndSeq;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmitQuat(YAML::Emitter& e, const glm::quat& q)
|
||||||
|
{
|
||||||
|
e << YAML::Flow << YAML::BeginSeq << q.w << q.x << q.y << q.z
|
||||||
|
<< YAML::EndSeq;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 读端辅助:缺字段/类型错/僵尸节点 → 默认值 ----
|
||||||
|
|
||||||
|
glm::vec3 ReadVec3(const YAML::Node& n, const glm::vec3& def)
|
||||||
|
{
|
||||||
|
if (!n || !n.IsSequence() || n.size() < 3) return def;
|
||||||
|
return {n[0].as<f32>(def.x), n[1].as<f32>(def.y),
|
||||||
|
n[2].as<f32>(def.z)};
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec4 ReadVec4(const YAML::Node& n, const glm::vec4& def)
|
||||||
|
{
|
||||||
|
if (!n || !n.IsSequence() || n.size() < 4) return def;
|
||||||
|
return {n[0].as<f32>(def.x), n[1].as<f32>(def.y),
|
||||||
|
n[2].as<f32>(def.z), n[3].as<f32>(def.w)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// [w,x,y,z] → 归一化 quat;非法/缺失回退单位四元数(防手改档出 NaN)
|
||||||
|
glm::quat ReadQuat(const YAML::Node& n)
|
||||||
|
{
|
||||||
|
if (!n || !n.IsSequence() || n.size() < 4) return {};
|
||||||
|
const f32 w = n[0].as<f32>(1.0f), x = n[1].as<f32>(0.0f);
|
||||||
|
const f32 y = n[2].as<f32>(0.0f), z = n[3].as<f32>(0.0f);
|
||||||
|
const f32 len = std::sqrt(x * x + y * y + z * z + w * w);
|
||||||
|
return len > 1e-8f ? glm::quat(w, x, y, z) / len : glm::quat{};
|
||||||
|
}
|
||||||
|
|
||||||
|
// "a,b,c[,d]" → N 个 int(procedural 伪路径参数解析)
|
||||||
|
template <size_t N>
|
||||||
|
bool ParseCsvInts(const std::string& s, int (&out)[N])
|
||||||
|
{
|
||||||
|
size_t pos = 0;
|
||||||
|
for (size_t i = 0; i < N; ++i) {
|
||||||
|
const size_t next =
|
||||||
|
(i + 1 < N) ? s.find(',', pos) : std::string::npos;
|
||||||
|
if (i + 1 < N && next == std::string::npos) return false;
|
||||||
|
try {
|
||||||
|
out[i] = std::stoi(s.substr(pos, next - pos));
|
||||||
|
}
|
||||||
|
catch (const std::exception&) { return false; }
|
||||||
|
pos = (next == std::string::npos) ? s.size() : next + 1;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// procedural:// 伪路径 → 参数反解重建(确定性键必然命中缓存;
|
||||||
|
// 绝不对伪路径调 LoadModel——那会走 assimp 必炸)
|
||||||
|
ModelRef ResolveProcedural(Assets::AssetManager& assets,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
constexpr std::string_view kPlane = "procedural://plane/";
|
||||||
|
constexpr std::string_view kTPlane =
|
||||||
|
"procedural://transparent-plane/";
|
||||||
|
|
||||||
|
if (path.rfind(kTPlane, 0) == 0) {
|
||||||
|
// "{halfSize}/{r},{g},{b},{a}"
|
||||||
|
const std::string rest = path.substr(kTPlane.size());
|
||||||
|
const auto slash = rest.find('/');
|
||||||
|
if (slash == std::string::npos) return {};
|
||||||
|
int c[4];
|
||||||
|
if (!ParseCsvInts(rest.substr(slash + 1), c)) return {};
|
||||||
|
try {
|
||||||
|
return assets.CreateTransparentPlane(
|
||||||
|
std::stof(rest.substr(0, slash)),
|
||||||
|
static_cast<u8>(c[0]), static_cast<u8>(c[1]),
|
||||||
|
static_cast<u8>(c[2]), static_cast<u8>(c[3]));
|
||||||
|
}
|
||||||
|
catch (const std::exception&) { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.rfind(kPlane, 0) == 0) {
|
||||||
|
// "{halfSize}/{r},{g},{b}"
|
||||||
|
const std::string rest = path.substr(kPlane.size());
|
||||||
|
const auto slash = rest.find('/');
|
||||||
|
if (slash == std::string::npos) return {};
|
||||||
|
int c[3];
|
||||||
|
if (!ParseCsvInts(rest.substr(slash + 1), c)) return {};
|
||||||
|
try {
|
||||||
|
return assets.CreatePlane(
|
||||||
|
std::stof(rest.substr(0, slash)),
|
||||||
|
static_cast<u8>(c[0]), static_cast<u8>(c[1]),
|
||||||
|
static_cast<u8>(c[2]));
|
||||||
|
}
|
||||||
|
catch (const std::exception&) { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}; // 未知伪路径
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模型引用解析降级链:procedural 反解 → GUID 主键 → Path 兜底。
|
||||||
|
// 加载成功后 GetOrCreateGUID 回写注册表(注册表丢档自愈)
|
||||||
|
ModelRef ResolveModel(Assets::AssetManager& assets, u64 guid,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
if (ModelRef ref = ResolveProcedural(assets, path); ref.Valid()) {
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string resolved;
|
||||||
|
if (guid != 0) resolved = assets.ResolveGUID(guid);
|
||||||
|
if (resolved.empty()) resolved = path;
|
||||||
|
if (resolved.empty()) return {};
|
||||||
|
|
||||||
|
ModelRef ref = assets.LoadModel(resolved);
|
||||||
|
if (ref.Valid()) assets.GetOrCreateGUID(resolved);
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 中间结构:解析全部成功才碰 registry ----
|
||||||
|
// comps 持 Components 节点(仅在键存在且为 map 时赋值——
|
||||||
|
// yaml-cpp 新版对僵尸节点做 operator= 会抛异常)
|
||||||
|
|
||||||
|
struct ParsedEntity
|
||||||
|
{
|
||||||
|
u64 uuid = 0;
|
||||||
|
std::string name = "entity";
|
||||||
|
YAML::Node comps; // 缺省 = Null(无组件)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 写端:单实体 → Emitter(调用方已进入 Entities 序列)
|
||||||
|
void EmitEntity(YAML::Emitter& out, Assets::AssetManager& assets,
|
||||||
|
const ECS::Registry& reg, entt::entity e)
|
||||||
|
{
|
||||||
|
const auto& id = reg.get<ECS::IdComponent>(e);
|
||||||
|
|
||||||
|
out << YAML::BeginMap;
|
||||||
|
out << YAML::Key << "Entity" << YAML::Value << id.uuid.Value();
|
||||||
|
if (const auto* name = reg.try_get<ECS::NameComponent>(e)) {
|
||||||
|
out << YAML::Key << "Name" << YAML::Value << name->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
out << YAML::Key << "Components" << YAML::Value << YAML::BeginMap;
|
||||||
|
|
||||||
|
if (const auto* t = reg.try_get<ECS::TransformComponent>(e)) {
|
||||||
|
out << YAML::Key << "Transform" << YAML::Value
|
||||||
|
<< YAML::BeginMap;
|
||||||
|
out << YAML::Key << "Position" << YAML::Value;
|
||||||
|
EmitVec3(out, t->GetPosition());
|
||||||
|
out << YAML::Key << "Rotation" << YAML::Value;
|
||||||
|
EmitQuat(out, t->GetRotationQuat()); // (w,x,y,z) 无损
|
||||||
|
out << YAML::Key << "Scale" << YAML::Value;
|
||||||
|
EmitVec3(out, t->GetScale());
|
||||||
|
out << YAML::EndMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto* mr = reg.try_get<ECS::MeshRendererComponent>(e)) {
|
||||||
|
// ModelRef → {Guid, Path}:Path 反查为空 = 无效引用,跳过
|
||||||
|
const std::string& path = assets.Models().Path(mr->model);
|
||||||
|
if (!path.empty()) {
|
||||||
|
out << YAML::Key << "MeshRenderer" << YAML::Value
|
||||||
|
<< YAML::BeginMap;
|
||||||
|
out << YAML::Key << "Model" << YAML::Value << YAML::Flow
|
||||||
|
<< YAML::BeginMap;
|
||||||
|
if (const u64 guid = assets.GetOrCreateGUID(path);
|
||||||
|
guid != 0) {
|
||||||
|
out << YAML::Key << "Guid" << YAML::Value << guid;
|
||||||
|
}
|
||||||
|
out << YAML::Key << "Path" << YAML::Value << path;
|
||||||
|
out << YAML::EndMap; // Model(flow)
|
||||||
|
out << YAML::Key << "Active" << YAML::Value << mr->active;
|
||||||
|
out << YAML::EndMap; // MeshRenderer
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ENGINE_WARN("SceneSerializer: entity has invalid model "
|
||||||
|
"ref, MeshRenderer skipped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto* a = reg.try_get<ECS::AnimationComponent>(e)) {
|
||||||
|
out << YAML::Key << "Animation" << YAML::Value << YAML::Flow
|
||||||
|
<< YAML::BeginMap;
|
||||||
|
out << YAML::Key << "ClipIndex" << YAML::Value << a->clipIndex;
|
||||||
|
out << YAML::Key << "Playing" << YAML::Value << a->playing;
|
||||||
|
out << YAML::Key << "Loop" << YAML::Value << a->loop;
|
||||||
|
out << YAML::Key << "Speed" << YAML::Value << a->speed;
|
||||||
|
out << YAML::Key << "Active" << YAML::Value << a->active;
|
||||||
|
out << YAML::EndMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto* c = reg.try_get<ECS::CameraComponent>(e)) {
|
||||||
|
out << YAML::Key << "Camera" << YAML::Value << YAML::Flow
|
||||||
|
<< YAML::BeginMap;
|
||||||
|
out << YAML::Key << "Primary" << YAML::Value << c->primary;
|
||||||
|
out << YAML::Key << "Active" << YAML::Value << c->active;
|
||||||
|
out << YAML::EndMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto* l = reg.try_get<ECS::LightComponent>(e)) {
|
||||||
|
out << YAML::Key << "Light" << YAML::Value << YAML::BeginMap;
|
||||||
|
out << YAML::Key << "Type" << YAML::Value
|
||||||
|
<< static_cast<int>(l->type);
|
||||||
|
out << YAML::Key << "Direction" << YAML::Value;
|
||||||
|
EmitVec3(out, l->direction);
|
||||||
|
out << YAML::Key << "Color" << YAML::Value;
|
||||||
|
EmitVec4(out, l->color);
|
||||||
|
out << YAML::Key << "Radius" << YAML::Value << l->radius;
|
||||||
|
out << YAML::Key << "CastShadow" << YAML::Value
|
||||||
|
<< l->castShadow;
|
||||||
|
out << YAML::Key << "Active" << YAML::Value << l->active;
|
||||||
|
out << YAML::EndMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto* rb = reg.try_get<ECS::RigidBodyComponent>(e)) {
|
||||||
|
out << YAML::Key << "RigidBody" << YAML::Value
|
||||||
|
<< YAML::BeginMap;
|
||||||
|
out << YAML::Key << "Type" << YAML::Value
|
||||||
|
<< static_cast<int>(rb->type);
|
||||||
|
out << YAML::Key << "Collider" << YAML::Value
|
||||||
|
<< static_cast<int>(rb->collider);
|
||||||
|
out << YAML::Key << "BoxHalfExtents" << YAML::Value;
|
||||||
|
EmitVec3(out, rb->boxHalfExtents);
|
||||||
|
out << YAML::Key << "SphereRadius" << YAML::Value
|
||||||
|
<< rb->sphereRadius;
|
||||||
|
out << YAML::Key << "CapsuleRadius" << YAML::Value
|
||||||
|
<< rb->capsuleRadius;
|
||||||
|
out << YAML::Key << "CapsuleHalfHeight" << YAML::Value
|
||||||
|
<< rb->capsuleHalfHeight;
|
||||||
|
out << YAML::Key << "CylinderRadius" << YAML::Value
|
||||||
|
<< rb->cylinderRadius;
|
||||||
|
out << YAML::Key << "CylinderHalfHeight" << YAML::Value
|
||||||
|
<< rb->cylinderHalfHeight;
|
||||||
|
if (!rb->compound.empty()) {
|
||||||
|
out << YAML::Key << "Compound" << YAML::Value
|
||||||
|
<< YAML::BeginSeq;
|
||||||
|
for (const auto& c : rb->compound) {
|
||||||
|
out << YAML::BeginMap;
|
||||||
|
out << YAML::Key << "Shape" << YAML::Value
|
||||||
|
<< static_cast<int>(c.shape);
|
||||||
|
out << YAML::Key << "BoxHalfExtents" << YAML::Value;
|
||||||
|
EmitVec3(out, c.boxHalfExtents);
|
||||||
|
out << YAML::Key << "Radius" << YAML::Value << c.radius;
|
||||||
|
out << YAML::Key << "CapsuleHalfHeight" << YAML::Value
|
||||||
|
<< c.capsuleHalfHeight;
|
||||||
|
out << YAML::Key << "Position" << YAML::Value;
|
||||||
|
EmitVec3(out, c.position);
|
||||||
|
out << YAML::Key << "Rotation" << YAML::Value;
|
||||||
|
EmitQuat(out, c.rotation); // (w,x,y,z)
|
||||||
|
out << YAML::EndMap;
|
||||||
|
}
|
||||||
|
out << YAML::EndSeq;
|
||||||
|
}
|
||||||
|
out << YAML::Key << "Mass" << YAML::Value << rb->mass;
|
||||||
|
out << YAML::Key << "GravityScale" << YAML::Value
|
||||||
|
<< rb->gravityScale;
|
||||||
|
out << YAML::Key << "Friction" << YAML::Value << rb->friction;
|
||||||
|
out << YAML::Key << "Restitution" << YAML::Value
|
||||||
|
<< rb->restitution;
|
||||||
|
out << YAML::Key << "IsTrigger" << YAML::Value << rb->isTrigger;
|
||||||
|
// bodyId/bodyValid/velocity 由 PhysicsWorld 对账重建,不存
|
||||||
|
out << YAML::Key << "Active" << YAML::Value << rb->active;
|
||||||
|
out << YAML::EndMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto* jc = reg.try_get<ECS::JointComponent>(e)) {
|
||||||
|
out << YAML::Key << "Joint" << YAML::Value
|
||||||
|
<< YAML::BeginMap;
|
||||||
|
out << YAML::Key << "Type" << YAML::Value
|
||||||
|
<< static_cast<int>(jc->type);
|
||||||
|
// 实体引用存 UUID(跨存档稳定;target=0 未设置)
|
||||||
|
out << YAML::Key << "Target" << YAML::Value
|
||||||
|
<< jc->target.Value();
|
||||||
|
out << YAML::Key << "AnchorA" << YAML::Value;
|
||||||
|
EmitVec3(out, jc->anchorA);
|
||||||
|
out << YAML::Key << "AnchorB" << YAML::Value;
|
||||||
|
EmitVec3(out, jc->anchorB);
|
||||||
|
out << YAML::Key << "HingeAxis" << YAML::Value;
|
||||||
|
EmitVec3(out, jc->hingeAxis);
|
||||||
|
out << YAML::Key << "EnableLimit" << YAML::Value
|
||||||
|
<< jc->enableLimit;
|
||||||
|
out << YAML::Key << "LimitMin" << YAML::Value << jc->limitMin;
|
||||||
|
out << YAML::Key << "LimitMax" << YAML::Value << jc->limitMax;
|
||||||
|
out << YAML::Key << "Active" << YAML::Value << jc->active;
|
||||||
|
out << YAML::EndMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 角色只存参数(速度/着地等状态不序列化)
|
||||||
|
if (const auto* cc = reg.try_get<ECS::CharacterControllerComponent>(e)) {
|
||||||
|
out << YAML::Key << "CharacterController" << YAML::Value
|
||||||
|
<< YAML::BeginMap;
|
||||||
|
out << YAML::Key << "Radius" << YAML::Value << cc->radius;
|
||||||
|
out << YAML::Key << "CapsuleHalfHeight" << YAML::Value
|
||||||
|
<< cc->capsuleHalfHeight;
|
||||||
|
out << YAML::Key << "Mass" << YAML::Value << cc->mass;
|
||||||
|
out << YAML::Key << "MaxSlopeAngle" << YAML::Value
|
||||||
|
<< cc->maxSlopeAngle;
|
||||||
|
out << YAML::Key << "StepHeight" << YAML::Value
|
||||||
|
<< cc->stepHeight;
|
||||||
|
out << YAML::Key << "Active" << YAML::Value << cc->active;
|
||||||
|
out << YAML::EndMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 脚本只存类名(注册表键);未注册名字加载后运行时警告跳过
|
||||||
|
if (const auto* sc = reg.try_get<ECS::ScriptComponent>(e)) {
|
||||||
|
out << YAML::Key << "Script" << YAML::Value << YAML::BeginMap;
|
||||||
|
out << YAML::Key << "ScriptName" << YAML::Value
|
||||||
|
<< sc->scriptName;
|
||||||
|
out << YAML::Key << "Active" << YAML::Value << sc->active;
|
||||||
|
out << YAML::EndMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
out << YAML::EndMap; // Components
|
||||||
|
out << YAML::EndMap; // Entity
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// ---- 保存 ----
|
||||||
|
|
||||||
|
bool SceneSerializer::Serialize(const ECS::Scene& scene,
|
||||||
|
Assets::AssetManager& assets,
|
||||||
|
const std::string& outPath,
|
||||||
|
const std::string& environmentPath)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// float32 无损往返需 9 位有效数字(std::numeric_limits
|
||||||
|
// <float>::max_digits10)
|
||||||
|
YAML::Emitter out;
|
||||||
|
out.SetFloatPrecision(9);
|
||||||
|
|
||||||
|
out << YAML::BeginMap;
|
||||||
|
out << YAML::Key << "Scene" << YAML::Value
|
||||||
|
<< std::filesystem::path(outPath).stem().string();
|
||||||
|
out << YAML::Key << "Version" << YAML::Value << kSceneVersion;
|
||||||
|
if (!environmentPath.empty()) {
|
||||||
|
out << YAML::Key << "Environment" << YAML::Value
|
||||||
|
<< environmentPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& s = scene.GetSettings();
|
||||||
|
out << YAML::Key << "Settings" << YAML::Value << YAML::BeginMap;
|
||||||
|
out << YAML::Key << "ClearColor" << YAML::Value;
|
||||||
|
EmitVec4(out, s.ClearColor);
|
||||||
|
out << YAML::Key << "AmbientColor" << YAML::Value;
|
||||||
|
EmitVec4(out, s.AmbientColor);
|
||||||
|
out << YAML::Key << "DrawSkybox" << YAML::Value << s.DrawSkybox;
|
||||||
|
out << YAML::EndMap;
|
||||||
|
|
||||||
|
out << YAML::Key << "Entities" << YAML::Value << YAML::BeginSeq;
|
||||||
|
const ECS::Registry& reg = scene.GetRegistry();
|
||||||
|
reg.view<ECS::IdComponent>().each(
|
||||||
|
[&](entt::entity e, const ECS::IdComponent&) {
|
||||||
|
EmitEntity(out, assets, reg, e);
|
||||||
|
});
|
||||||
|
out << YAML::EndSeq;
|
||||||
|
out << YAML::EndMap;
|
||||||
|
|
||||||
|
if (!out.good()) {
|
||||||
|
ENGINE_ERROR("SceneSerializer: emit failed: {}", out.GetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream file(outPath, std::ios::trunc);
|
||||||
|
if (!file) {
|
||||||
|
ENGINE_ERROR("SceneSerializer: cannot open '{}' for write",
|
||||||
|
outPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
file << out.c_str();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (const YAML::Exception& e) {
|
||||||
|
ENGINE_ERROR("SceneSerializer: save failed ({})", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (const std::exception& e) {
|
||||||
|
ENGINE_ERROR("SceneSerializer: save failed ({})", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 加载 ----
|
||||||
|
|
||||||
|
bool SceneSerializer::Deserialize(ECS::Scene& scene,
|
||||||
|
Assets::AssetManager& assets,
|
||||||
|
const std::string& inPath,
|
||||||
|
std::string& outEnvironmentPath)
|
||||||
|
{
|
||||||
|
outEnvironmentPath.clear();
|
||||||
|
|
||||||
|
YAML::Node root;
|
||||||
|
try {
|
||||||
|
root = YAML::LoadFile(inPath);
|
||||||
|
}
|
||||||
|
catch (const YAML::Exception& e) {
|
||||||
|
ENGINE_ERROR("SceneSerializer: cannot parse '{}' ({})", inPath,
|
||||||
|
e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (const std::exception& e) {
|
||||||
|
ENGINE_ERROR("SceneSerializer: cannot read '{}' ({})", inPath,
|
||||||
|
e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Version 闸:高版本拒载(结构未知),缺失按 0 宽容旧档
|
||||||
|
if (const int version = root["Version"].as<int>(0);
|
||||||
|
version > kSceneVersion) {
|
||||||
|
ENGINE_ERROR("SceneSerializer: '{}' version {} > supported {}",
|
||||||
|
inPath, version, kSceneVersion);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 僵尸节点安全:as<T>(fallback) 对缺失键返回 fallback
|
||||||
|
outEnvironmentPath = root["Environment"].as<std::string>("");
|
||||||
|
|
||||||
|
if (const YAML::Node st = root["Settings"]; st && st.IsMap()) {
|
||||||
|
auto& s = scene.GetSettings();
|
||||||
|
s.ClearColor = ReadVec4(st["ClearColor"], s.ClearColor);
|
||||||
|
s.AmbientColor = ReadVec4(st["AmbientColor"], s.AmbientColor);
|
||||||
|
s.DrawSkybox = st["DrawSkybox"].as<bool>(s.DrawSkybox);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 阶段 1:解析到中间结构(逐实体容错,不动 registry)----
|
||||||
|
std::vector<ParsedEntity> parsed;
|
||||||
|
|
||||||
|
const YAML::Node entities = root["Entities"];
|
||||||
|
if (entities && entities.IsSequence()) {
|
||||||
|
parsed.reserve(entities.size());
|
||||||
|
for (const auto& e : entities) {
|
||||||
|
try {
|
||||||
|
ParsedEntity pe;
|
||||||
|
pe.uuid = e["Entity"].as<u64>(0);
|
||||||
|
pe.name = e["Name"].as<std::string>(pe.name);
|
||||||
|
if (const YAML::Node comps = e["Components"];
|
||||||
|
comps && comps.IsMap()) {
|
||||||
|
// 仅在 rhs 有效时赋值(僵尸节点 operator= 会抛)
|
||||||
|
pe.comps = comps;
|
||||||
|
}
|
||||||
|
parsed.push_back(std::move(pe));
|
||||||
|
}
|
||||||
|
catch (const std::exception& err) {
|
||||||
|
// 单实体损坏不炸全场:跳过继续
|
||||||
|
ENGINE_WARN("SceneSerializer: entity skipped ({})",
|
||||||
|
err.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 阶段 2:原子换血重建 ----
|
||||||
|
scene.Clear();
|
||||||
|
|
||||||
|
std::unordered_set<u64> seenUuids;
|
||||||
|
for (ParsedEntity& pe : parsed) {
|
||||||
|
auto handle = scene.CreateEntity(pe.name);
|
||||||
|
|
||||||
|
// UUID 重复/无效 → 重新生成(Prefab 实例化的 ID 重映射语义)
|
||||||
|
u64 uuid = pe.uuid;
|
||||||
|
if (uuid == 0 || !seenUuids.insert(uuid).second) {
|
||||||
|
uuid = UUID::New().Value();
|
||||||
|
}
|
||||||
|
handle.GetComponent<ECS::IdComponent>().uuid = UUID{uuid};
|
||||||
|
|
||||||
|
const YAML::Node comps = pe.comps; // 拷贝初始化,缺省 Null
|
||||||
|
|
||||||
|
if (const YAML::Node n = comps["Transform"]) {
|
||||||
|
auto& t = handle.GetComponent<ECS::TransformComponent>();
|
||||||
|
t.SetPosition(ReadVec3(n["Position"], t.GetPosition()));
|
||||||
|
t.SetRotationQuat(ReadQuat(n["Rotation"]));
|
||||||
|
t.SetScale(ReadVec3(n["Scale"], t.GetScale()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const YAML::Node n = comps["MeshRenderer"]) {
|
||||||
|
if (const YAML::Node m = n["Model"]) {
|
||||||
|
const u64 guid = m["Guid"].as<u64>(0);
|
||||||
|
const auto path = m["Path"].as<std::string>("");
|
||||||
|
if (ModelRef ref = ResolveModel(assets, guid, path);
|
||||||
|
ref.Valid()) {
|
||||||
|
handle.AddComponent<ECS::MeshRendererComponent>(
|
||||||
|
ECS::MeshRendererComponent{ref});
|
||||||
|
handle.GetComponent<ECS::MeshRendererComponent>().active =
|
||||||
|
n["Active"].as<bool>(true); // 缺省 true 向前兼容
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ENGINE_WARN("SceneSerializer: model unresolved "
|
||||||
|
"(guid={}, path='{}'), MeshRenderer "
|
||||||
|
"skipped",
|
||||||
|
guid, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const YAML::Node n = comps["Animation"]) {
|
||||||
|
auto& a = handle.AddComponent<ECS::AnimationComponent>();
|
||||||
|
a.clipIndex = n["ClipIndex"].as<int>(a.clipIndex);
|
||||||
|
a.playing = n["Playing"].as<bool>(a.playing);
|
||||||
|
a.loop = n["Loop"].as<bool>(a.loop);
|
||||||
|
a.speed = n["Speed"].as<f32>(a.speed);
|
||||||
|
a.active = n["Active"].as<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const YAML::Node n = comps["Camera"]) {
|
||||||
|
auto& c = handle.AddComponent<ECS::CameraComponent>();
|
||||||
|
c.primary = n["Primary"].as<bool>(false);
|
||||||
|
c.active = n["Active"].as<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const YAML::Node n = comps["Light"]) {
|
||||||
|
auto& l = handle.AddComponent<ECS::LightComponent>();
|
||||||
|
if (const int type = n["Type"].as<int>(0); type >= 0 &&
|
||||||
|
type <= 1) {
|
||||||
|
l.type = static_cast<ECS::LightComponent::Type>(type);
|
||||||
|
}
|
||||||
|
l.direction = ReadVec3(n["Direction"], l.direction);
|
||||||
|
l.color = ReadVec4(n["Color"], l.color);
|
||||||
|
l.radius = n["Radius"].as<f32>(l.radius);
|
||||||
|
l.castShadow = n["CastShadow"].as<bool>(l.castShadow);
|
||||||
|
l.active = n["Active"].as<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const YAML::Node n = comps["RigidBody"]) {
|
||||||
|
auto& rb = handle.AddComponent<ECS::RigidBodyComponent>();
|
||||||
|
if (const int type = n["Type"].as<int>(0);
|
||||||
|
type >= 0 && type <= 2) {
|
||||||
|
rb.type =
|
||||||
|
static_cast<ECS::RigidBodyComponent::BodyType>(type);
|
||||||
|
}
|
||||||
|
if (const int col = n["Collider"].as<int>(0);
|
||||||
|
col >= 0 && col <= 5) {
|
||||||
|
rb.collider =
|
||||||
|
static_cast<ECS::RigidBodyComponent::ColliderType>(
|
||||||
|
col);
|
||||||
|
}
|
||||||
|
rb.boxHalfExtents = ReadVec3(n["BoxHalfExtents"],
|
||||||
|
rb.boxHalfExtents);
|
||||||
|
rb.sphereRadius = n["SphereRadius"].as<f32>(rb.sphereRadius);
|
||||||
|
rb.capsuleRadius = n["CapsuleRadius"].as<f32>(rb.capsuleRadius);
|
||||||
|
rb.capsuleHalfHeight =
|
||||||
|
n["CapsuleHalfHeight"].as<f32>(rb.capsuleHalfHeight);
|
||||||
|
rb.cylinderRadius =
|
||||||
|
n["CylinderRadius"].as<f32>(rb.cylinderRadius);
|
||||||
|
rb.cylinderHalfHeight =
|
||||||
|
n["CylinderHalfHeight"].as<f32>(rb.cylinderHalfHeight);
|
||||||
|
// 僵尸节点铁律:拷贝初始化 + 判空再遍历(缺 Compound 键安全)
|
||||||
|
if (const YAML::Node cs = n["Compound"]) {
|
||||||
|
for (const auto& item : cs) {
|
||||||
|
ECS::RigidBodyComponent::CompoundChild c;
|
||||||
|
if (const int sh = item["Shape"].as<int>(0);
|
||||||
|
sh >= 0 && sh <= 2) {
|
||||||
|
c.shape = static_cast<
|
||||||
|
ECS::RigidBodyComponent::CompoundChild::Shape>(
|
||||||
|
sh);
|
||||||
|
}
|
||||||
|
c.boxHalfExtents = ReadVec3(item["BoxHalfExtents"],
|
||||||
|
c.boxHalfExtents);
|
||||||
|
c.radius = item["Radius"].as<f32>(c.radius);
|
||||||
|
c.capsuleHalfHeight =
|
||||||
|
item["CapsuleHalfHeight"].as<f32>(
|
||||||
|
c.capsuleHalfHeight);
|
||||||
|
c.position = ReadVec3(item["Position"], c.position);
|
||||||
|
c.rotation = ReadQuat(item["Rotation"]);
|
||||||
|
rb.compound.push_back(std::move(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rb.mass = n["Mass"].as<f32>(rb.mass);
|
||||||
|
rb.gravityScale = n["GravityScale"].as<f32>(rb.gravityScale);
|
||||||
|
rb.friction = n["Friction"].as<f32>(rb.friction);
|
||||||
|
rb.restitution = n["Restitution"].as<f32>(rb.restitution);
|
||||||
|
// 新键 IsTrigger;旧场景 IsSensor 兼容读(缺键 as 返回 fallback 安全)
|
||||||
|
rb.isTrigger = n["IsTrigger"].as<bool>(
|
||||||
|
n["IsSensor"].as<bool>(rb.isTrigger));
|
||||||
|
rb.active = n["Active"].as<bool>(true);
|
||||||
|
// bodyId/bodyValid/velocity 保持默认(对账重建)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Joint:target 只存 UUID,加载后由 PhysicsWorld 惰性反查
|
||||||
|
//(两遍解析不需要——UUID 再生/加载顺序均由逐帧解析消化)
|
||||||
|
if (const YAML::Node n = comps["Joint"]) {
|
||||||
|
auto& jc = handle.AddComponent<ECS::JointComponent>();
|
||||||
|
if (const int type = n["Type"].as<int>(0);
|
||||||
|
type >= 0 && type <= 2) {
|
||||||
|
jc.type =
|
||||||
|
static_cast<ECS::JointComponent::Type>(type);
|
||||||
|
}
|
||||||
|
jc.target = UUID{n["Target"].as<u64>(0)};
|
||||||
|
jc.anchorA = ReadVec3(n["AnchorA"], jc.anchorA);
|
||||||
|
jc.anchorB = ReadVec3(n["AnchorB"], jc.anchorB);
|
||||||
|
jc.hingeAxis = ReadVec3(n["HingeAxis"], jc.hingeAxis);
|
||||||
|
jc.enableLimit = n["EnableLimit"].as<bool>(jc.enableLimit);
|
||||||
|
jc.limitMin = n["LimitMin"].as<f32>(jc.limitMin);
|
||||||
|
jc.limitMax = n["LimitMax"].as<f32>(jc.limitMax);
|
||||||
|
jc.active = n["Active"].as<bool>(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const YAML::Node n = comps["CharacterController"]) {
|
||||||
|
auto& cc =
|
||||||
|
handle.AddComponent<ECS::CharacterControllerComponent>();
|
||||||
|
cc.radius = n["Radius"].as<f32>(cc.radius);
|
||||||
|
cc.capsuleHalfHeight =
|
||||||
|
n["CapsuleHalfHeight"].as<f32>(cc.capsuleHalfHeight);
|
||||||
|
cc.mass = n["Mass"].as<f32>(cc.mass);
|
||||||
|
cc.maxSlopeAngle = n["MaxSlopeAngle"].as<f32>(cc.maxSlopeAngle);
|
||||||
|
cc.stepHeight = n["StepHeight"].as<f32>(cc.stepHeight);
|
||||||
|
cc.active = n["Active"].as<bool>(true);
|
||||||
|
// 状态(速度/着地)由 PhysicsWorld 对账重建,不存
|
||||||
|
}
|
||||||
|
|
||||||
|
// 脚本:只存类名;未注册名字由 ScriptRuntime 运行时警告跳过
|
||||||
|
if (const YAML::Node n = comps["Script"]) {
|
||||||
|
auto& sc = handle.AddComponent<ECS::ScriptComponent>();
|
||||||
|
sc.scriptName = n["ScriptName"].as<std::string>("");
|
||||||
|
sc.active = n["Active"].as<bool>(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms:
|
||||||
|
// 显式补算,否则首帧 World = 单位阵
|
||||||
|
scene.UpdateTransforms();
|
||||||
|
|
||||||
|
ENGINE_INFO("SceneSerializer: loaded {} entities from '{}'",
|
||||||
|
parsed.size(), inPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Engine::Serialization
|
||||||
Submodule
+1
Submodule Lib/yaml-cpp added at 1e0876c671
Reference in New Issue
Block a user