diff --git a/App/editor/panels/AssetBrowserPanel.cpp b/App/editor/panels/AssetBrowserPanel.cpp index b23ab96..ddbaf91 100644 --- a/App/editor/panels/AssetBrowserPanel.cpp +++ b/App/editor/panels/AssetBrowserPanel.cpp @@ -315,6 +315,16 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx, ImGui::EndDragDropSource(); } + // 模型拖拽源:拖到 Outliner 落点创建带 MeshRenderer 的实体 + //(kModelDragPayload;实体名 = 文件名) + if (item.kind == Kind::Model && ImGui::BeginDragDropSource()) + { + ImGui::SetDragDropPayload(kModelDragPayload, + assetPath.c_str(), assetPath.size() + 1); + ImGui::TextUnformatted(name.c_str()); + ImGui::EndDragDropSource(); + } + // 双击:.material 打开 Material Editor;.animctrl 打开 // Controller Editor;model/HDR 进缓存预热(路径去重,重复 // 双击零成本);场景加载含环境恢复/选中清理流程,提示走 @@ -404,21 +414,26 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx, std::localtime(&tt)); ImGui::Text("modified: %s", tmBuf); } - // 加载状态(只查询不触发加载;model/HDR 进过缓存才显示 loaded) + // 加载状态:只对"加载"型资产有意义(查询不触发加载)。 + // Prefab 是实例化模板、Scene 走 File 菜单——显示 "not + // loaded" 会误导(曾让用户当成 bug) if (ctx.assets) { - const char* state = "not loaded"; - if (item.kind == Kind::Model && - ctx.assets->IsModelLoaded(assetPath)) + if (item.kind == Kind::Model || + item.kind == Kind::Texture) { - state = "loaded"; + const bool loaded = + (item.kind == Kind::Model) + ? ctx.assets->IsModelLoaded(assetPath) + : ctx.assets->IsEnvironmentLoaded(assetPath); + ImGui::Text("state: %s", loaded ? "loaded" + : "not loaded"); } - else if (item.kind == Kind::Texture && - ctx.assets->IsEnvironmentLoaded(assetPath)) + else if (item.kind == Kind::Prefab) { - state = "loaded"; + ImGui::TextDisabled( + "template — drag into scene / double-click"); } - ImGui::Text("state: %s", state); // 已加载的 model 展开资产数据概览(meshes=0 即纯动画 // 容器;未加载不显示——查询不触发加载) if (item.kind == Kind::Model) diff --git a/App/editor/panels/AssetBrowserPanel.h b/App/editor/panels/AssetBrowserPanel.h index 802dc22..0a6eae6 100644 --- a/App/editor/panels/AssetBrowserPanel.h +++ b/App/editor/panels/AssetBrowserPanel.h @@ -23,6 +23,10 @@ // 常量住源面板头 inline constexpr const char* kPrefabDragPayload = "PREFAB_PATH"; +// 模型资产拖拽 payload(Content Browser 拖 .glb/.fbx/... → Outliner +// 落点创建带 MeshRenderer 的实体;字符串 = 资产相对路径) +inline constexpr const char* kModelDragPayload = "MODEL_PATH"; + class AssetBrowserPanel { public: void Draw(const EditorContext& ctx); diff --git a/App/editor/panels/HierarchyPanel.cpp b/App/editor/panels/HierarchyPanel.cpp index fd41e1e..2012a37 100644 --- a/App/editor/panels/HierarchyPanel.cpp +++ b/App/editor/panels/HierarchyPanel.cpp @@ -49,6 +49,30 @@ namespace { *ctx.scene, *ctx.assets, path); } + // 模型拖入落点:创建带 MeshRenderer 的单实体(名字 = 文件名 + // stem,默认位姿;模型句柄 = LoadModel 磁盘路径)。拖到节点下 = + // SetParent keepWorld;空白区 = 场景根。加载失败不建空实体 + entt::entity SpawnModelDrop(const EditorContext& ctx, const char* path) + { + if (!ctx.scene || !ctx.assets || !path) { + return entt::null; + } + const auto model = ctx.assets->LoadModel(path); + if (!model.Valid()) { + return entt::null; + } + std::string name = + std::filesystem::path(path).stem().string(); + if (name.empty()) { + name = "model entity"; + } + auto e = ctx.scene->CreateEntity(name); + e.AddComponent().model = model; + ENGINE_INFO("Hierarchy: model entity '{}' created from '{}'", + name, path); + return static_cast(e); + } + // 实体名 → 合法文件名(路径分隔符/保留符净化;空名兜底) std::string PrefabFileName(std::string name) { @@ -136,6 +160,15 @@ namespace { selected = root; } } + if (const ImGuiPayload* pl = + ImGui::AcceptDragDropPayload(kModelDragPayload)) { + if (const entt::entity me = SpawnModelDrop( + ctx, static_cast(pl->Data)); + me != entt::null) { + ctx.scene->SetParent(me, e); // keepWorld 默认 true + selected = me; + } + } ImGui::EndDragDropTarget(); } @@ -273,6 +306,14 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected) selected = root; } } + if (const ImGuiPayload* pl = + ImGui::AcceptDragDropPayload(kModelDragPayload)) { + if (const entt::entity me = SpawnModelDrop( + ctx, static_cast(pl->Data)); + me != entt::null) { + selected = me; + } + } ImGui::EndDragDropTarget(); } } diff --git a/App/editor/panels/InspectorPanel.cpp b/App/editor/panels/InspectorPanel.cpp index eb83239..4c7c670 100644 --- a/App/editor/panels/InspectorPanel.cpp +++ b/App/editor/panels/InspectorPanel.cpp @@ -18,6 +18,7 @@ #include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面) #include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names) #include "Engine/Scripting/ScriptRuntime.h" // 参数编辑 → RefreshParams +#include "Engine/Serialization/SceneSerializer.h" // ResolveProcedural(程序化入口) #include "Engine/Core/Log.h" // Create Override 失败告警 #include @@ -670,6 +671,67 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) mr->materialOverrides.clear(); } ImGui::Spacing(); + // 程序化模型入口:伪资产不在磁盘(下拉框列不到),由 + // 弹窗组键生成 → ResolveProcedural(与场景反解同一 + // 键格式唯一定义点;确定性键必然命中缓存) + if (ImGui::SmallButton("Procedural...")) + { + ImGui::OpenPopup("##proc_model"); + } + if (ImGui::BeginPopup("##proc_model")) + { + static int shape = 0; // 0=cube 1=plane 2=transparent plane + static float half = 0.5f; + static float col[3] = {0.55f, 0.36f, 0.28f}; + static float alpha = 0.5f; + ImGui::RadioButton("Cube", &shape, 0); + ImGui::SameLine(); + ImGui::RadioButton("Plane", &shape, 1); + ImGui::SameLine(); + ImGui::RadioButton("Trans.Plane", &shape, 2); + ImGui::SetNextItemWidth(-1.0f); + ImGui::SliderFloat("half size", &half, 0.01f, 10.0f, + "%.2f"); + ImGui::ColorEdit3("color", col); + if (shape == 2) + { + ImGui::SetNextItemWidth(-1.0f); + ImGui::SliderFloat("alpha", &alpha, 0.0f, 1.0f, + "%.2f"); + } + if (ImGui::Button("Create & Assign")) + { + const int r = int(col[0] * 255.0f + 0.5f); + const int g = int(col[1] * 255.0f + 0.5f); + const int b = int(col[2] * 255.0f + 0.5f); + char key[96]; + if (shape == 2) + { + const int a = int(alpha * 255.0f + 0.5f); + std::snprintf( + key, sizeof(key), + "procedural://transparent-plane/%.3g/%d,%d,%d,%d", + half, r, g, b, a); + } + else + { + std::snprintf( + key, sizeof(key), + "procedural://%s/%.3g/%d,%d,%d", + shape == 0 ? "cube" : "plane", half, r, g, b); + } + if (auto ref = Engine::Serialization:: + SceneSerializer::ResolveProcedural( + *ctx.assets, key); + ref.Valid()) + { + mr->model = ref; + mr->materialOverrides.clear(); + ImGui::CloseCurrentPopup(); + } + } + ImGui::EndPopup(); + } } // 无效句柄不进 GetModel(缓存对无效/悬空句柄 FATAL,是 bug 信号) if (mr->model.Valid()) @@ -1951,6 +2013,10 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) // CrossFade 与 Direct 的 crossfade 共用 fade) ImGui::SliderFloat("Speed", &anim->speed, -2.0f, 2.0f); ImGui::SliderFloat("Fade", &anim->fadeDuration, 0.0f, 1.0f); + // 根运动(Unity Animator 同名开关):关 = 原地播放, + // 位移归脚本/物理驱动(CharacterController 角色用) + ImGui::Checkbox("Apply Root Motion", + &anim->applyRootMotion); // 动画数据表:每个 clip 的时长与有效轨道数(attach 进来的 // 外部动画在此核对骨骼匹配结果——轨道数明显偏少 = 骨骼 // 命名不同源被跳过) diff --git a/Engine/include/Engine/Serialization/SceneSerializer.h b/Engine/include/Engine/Serialization/SceneSerializer.h index f6f15b6..31c7150 100644 --- a/Engine/include/Engine/Serialization/SceneSerializer.h +++ b/Engine/include/Engine/Serialization/SceneSerializer.h @@ -24,6 +24,7 @@ #define HELLOVULKAN_SCENESERIALIZER_H #include // Entity + glm vec3/quat(Prefab API 签名用) +#include // ModelRef(ResolveProcedural 返回值) #include @@ -93,6 +94,13 @@ namespace Engine::Serialization { const std::string& inPath, const glm::vec3* overridePos = nullptr, const glm::quat* overrideRot = nullptr); + + // procedural:// 伪路径 → 参数反解重建(cube/plane/ + // transparent-plane)。确定性键必然命中缓存;绝不走 assimp。 + // 未知/格式坏伪路径返回无效句柄。场景加载与编辑器程序化 + // 入口共用——键格式唯一定义点 + static ModelRef ResolveProcedural( + Assets::AssetManager& assets, const std::string& path); }; } // namespace Engine::Serialization diff --git a/Engine/src/Serialization/SceneSerializer.cpp b/Engine/src/Serialization/SceneSerializer.cpp index a81115a..309665e 100644 --- a/Engine/src/Serialization/SceneSerializer.cpp +++ b/Engine/src/Serialization/SceneSerializer.cpp @@ -179,71 +179,17 @@ namespace Engine::Serialization { // 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/"; - constexpr std::string_view kCube = "procedural://cube/"; - - if (path.rfind(kCube, 0) == 0) { - // "{halfSize}/{r},{g},{b}" - const std::string rest = path.substr(kCube.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.CreateCube( - std::stof(rest.substr(0, slash)), - static_cast(c[0]), static_cast(c[1]), - static_cast(c[2])); - } - catch (const std::exception&) { return {}; } - } - - 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(c[0]), static_cast(c[1]), - static_cast(c[2]), static_cast(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(c[0]), static_cast(c[1]), - static_cast(c[2])); - } - catch (const std::exception&) { return {}; } - } - - return {}; // 未知伪路径 - } + // 实现见 SceneSerializer::ResolveProcedural(公共静态,编辑器 + // 程序化入口共用) // 模型引用解析降级链:procedural 反解 → GUID 主键 → Path 兜底。 // 加载成功后 GetOrCreateGUID 回写注册表(注册表丢档自愈) ModelRef ResolveModel(Assets::AssetManager& assets, u64 guid, const std::string& path) { - if (ModelRef ref = ResolveProcedural(assets, path); ref.Valid()) { + if (ModelRef ref = + SceneSerializer::ResolveProcedural(assets, path); + ref.Valid()) { return ref; } @@ -1245,6 +1191,67 @@ namespace Engine::Serialization { } } + // procedural:// 伪路径 → 参数反解重建(键格式唯一定义点; + // 绝不对伪路径调 LoadModel——那会走 assimp 必炸) + ModelRef SceneSerializer::ResolveProcedural( + Assets::AssetManager& assets, const std::string& path) + { + constexpr std::string_view kPlane = "procedural://plane/"; + constexpr std::string_view kTPlane = + "procedural://transparent-plane/"; + constexpr std::string_view kCube = "procedural://cube/"; + + if (path.rfind(kCube, 0) == 0) { + // "{halfSize}/{r},{g},{b}" + const std::string rest = path.substr(kCube.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.CreateCube( + std::stof(rest.substr(0, slash)), + static_cast(c[0]), static_cast(c[1]), + static_cast(c[2])); + } + catch (const std::exception&) { return {}; } + } + + 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(c[0]), static_cast(c[1]), + static_cast(c[2]), static_cast(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(c[0]), static_cast(c[1]), + static_cast(c[2])); + } + catch (const std::exception&) { return {}; } + } + + return {}; // 未知伪路径 + } + ECS::Entity SceneSerializer::InstantiatePrefab( ECS::Scene& scene, Assets::AssetManager& assets, const std::string& inPath,