阶段性合并:动画重定向、碎裂共享库、第三人称玩法、编辑器面板与 vendored ufbx

- 动画:HumanoidRig 三层架构(standard 标准槽 / dialects 方言映射 / mapper 配对),
  指/趾统一走世界增量两遍式烘焙;AnimationAttach 拆为纯算法 + facade;修正 HumanoidBasis
  行走基朝向与跨骨架去重、根运动通道
- 物理:CharacterController 角色互碰撞 / shapeOffset / 接触事件;PhysicsWorld 接触合成与
  字符冲量判定
- 渲染与材质:Model 多材质按材质拆子 mesh 与贴图路径解析、DDS 直传、GLB 内嵌纹理落盘、
  不透明通道 alpha 固定 1.0、glTF FlipUVs 对齐;SceneSerializer 支持 meshIndex 与材质覆盖
- 脚本:FractureLib 抽为共享库(HostView 桥接)、WallFracture 参数直绑、新增
  ThirdPersonCamera / Follower / WASDMove 玩法;移除 GameDirector / PlayerController / Target
- 编辑器:新增 GamepadPanel、LogPanel(LogRing),改进 Inspector / 材质 / 统计 / 视口面板
- 依赖:vendored ufbx(FBX 5.x/6.x/7.x 全版本解析,替代 assimp 对 FBX 6.1 的拒读)
This commit is contained in:
2026-10-11 22:22:19 +08:00
parent 334819bf97
commit beba491bcb
57 changed files with 43200 additions and 1378 deletions
+19
View File
@@ -19,6 +19,8 @@
#include "panels/ControllerEditorPanel.h"
#include "panels/ViewportPanel.h"
#include "panels/StatsPanel.h"
#include "panels/LogPanel.h"
#include "panels/GamepadPanel.h"
#include <Engine/Core/Log.h>
#include <Engine/Serialization/SceneSerializer.h>
@@ -60,6 +62,8 @@ void Editor::Init(const EditorContext& ctx)
m_Inspector = std::make_unique<InspectorPanel>();
m_Viewport = std::make_unique<ViewportPanel>();
m_Stats = std::make_unique<StatsPanel>();
m_Log = std::make_unique<LogPanel>();
m_Gamepad = std::make_unique<GamepadPanel>();
m_AssetBrowser = std::make_unique<AssetBrowserPanel>();
m_MaterialEditor = std::make_unique<MaterialEditorPanel>();
m_Ctx.materialEditor = m_MaterialEditor.get();
@@ -219,6 +223,8 @@ void Editor::Draw()
}
}
if (m_ShowStats) m_Stats->Draw(m_Ctx);
if (m_ShowLog) m_Log->Draw(m_Ctx);
m_Gamepad->Draw(m_Ctx); // Tools 弹窗(内部 m_Open 门控)
if (m_ShowAssetBrowser) m_AssetBrowser->Draw(m_Ctx);
if (m_ShowMaterialEditor) m_MaterialEditor->Draw(m_Ctx);
m_ControllerEditor->Draw(m_Ctx); // 面板内部 m_Open 门控
@@ -236,6 +242,11 @@ bool Editor::BlockingKeyboard() const
const ImGuiIO& io = ImGui::GetIO();
if (io.WantTextInput) return true; // 打字中(改名等)不让位快捷键
// PIE 运行态:键盘全给游戏——编辑热键/Gizmo 在运行态本就让位
//(ViewportPanel !playing 分支),不再因视口失焦拦截。否则点
// Play 后焦点停在工具栏,WASD 整局全死(玩家输入无响应的根因)
if (IsPlaying()) return false;
// 游戏形态(Fullscreen):WASD 全窗口生效,键盘只让位文本输入
if (m_Ctx.renderer &&
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
@@ -298,11 +309,18 @@ void Editor::DrawMenuBar()
ImGui::MenuItem("Details", nullptr, &m_ShowInspector);
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
ImGui::MenuItem("Log", nullptr, &m_ShowLog);
ImGui::MenuItem("Content Browser", nullptr, &m_ShowAssetBrowser);
ImGui::MenuItem("Material Editor", nullptr, &m_ShowMaterialEditor);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Tools"))
{
ImGui::MenuItem("Gamepad Test", nullptr, &m_Gamepad->m_Open);
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
@@ -428,6 +446,7 @@ void Editor::DrawDockSpace()
ImGui::DockBuilderDockWindow("Outliner", dockLeft);
ImGui::DockBuilderDockWindow("Viewport", dockCenter);
ImGui::DockBuilderDockWindow("Stats", dockCenter); // Viewport 旁 tab
ImGui::DockBuilderDockWindow("Log", dockBottom); // Content Browser 旁 tab
ImGui::DockBuilderDockWindow("Content Browser", dockBottom);
ImGui::DockBuilderDockWindow("Details", dockRight);
ImGui::DockBuilderDockWindow("Material Editor", dockRight); // Details 旁 tab
+5
View File
@@ -29,6 +29,8 @@ class HierarchyPanel;
class InspectorPanel;
class ViewportPanel;
class StatsPanel;
class LogPanel;
class GamepadPanel;
class AssetBrowserPanel;
class MaterialEditorPanel;
class ControllerEditorPanel;
@@ -126,6 +128,8 @@ private:
std::unique_ptr<InspectorPanel> m_Inspector;
std::unique_ptr<ViewportPanel> m_Viewport;
std::unique_ptr<StatsPanel> m_Stats;
std::unique_ptr<LogPanel> m_Log;
std::unique_ptr<GamepadPanel> m_Gamepad;
std::unique_ptr<AssetBrowserPanel> m_AssetBrowser;
std::unique_ptr<MaterialEditorPanel> m_MaterialEditor;
std::unique_ptr<ControllerEditorPanel> m_ControllerEditor;
@@ -137,6 +141,7 @@ private:
bool m_ShowInspector = true;
bool m_ShowViewport = true;
bool m_ShowStats = true;
bool m_ShowLog = true;
bool m_ShowAssetBrowser = true;
bool m_ShowMaterialEditor = true;
+199
View File
@@ -0,0 +1,199 @@
//
// GamepadPanel.cpp:手柄输入可视化回显(极简手柄视图)。
//
// 一块画布 = 手柄俯视图:顶部扳机条 + 肩键,左上左摇杆、左下十字键、
// 右上 ABXY(Xbox 语义色)、右下右摇杆,中央 Back/Guide/Start 小键。
// 按下即亮蓝(面键亮语义色),无数值无卡片——位置即实体,一眼对上
// 手里的手柄。数据源 = Window::PollGamepad(GLFW 原始轴值/按键)。
//
#include "GamepadPanel.h"
#include <Engine/Core/Window.h>
#include <imgui.h>
#include <cmath>
namespace {
// GLFW 标准布局下标
enum PadBtn {
kA = 0, kB = 1, kX = 2, kY = 3,
kLB = 4, kRB = 5, kBack = 6, kStart = 7, kGuide = 8,
kLS = 9, kRS = 10, kUp = 11, kRight = 12, kDown = 13, kLeft = 14,
};
constexpr ImU32 kLit = IM_COL32(90, 170, 250, 255); // 按下亮蓝
constexpr ImU32 kFill = IM_COL32(52, 52, 60, 255); // 暗态填充
constexpr ImU32 kEdge = IM_COL32(104, 104, 114, 255); // 暗态描边
constexpr ImU32 kTxt = IM_COL32(215, 215, 220, 255); // 标签文字
// 块状键(肩键 / Back / Start)
void Block(ImDrawList* dl, const ImVec2& c, float w, float h,
const char* label, bool pressed)
{
const ImVec2 mn(c.x - w * 0.5f, c.y - h * 0.5f);
const ImVec2 mx(c.x + w * 0.5f, c.y + h * 0.5f);
dl->AddRectFilled(mn, mx, pressed ? kLit : kFill, 5.0f);
dl->AddRect(mn, mx, pressed ? IM_COL32(160, 210, 255, 255) : kEdge,
5.0f);
const ImVec2 ts = ImGui::CalcTextSize(label);
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f), kTxt, label);
}
// 圆键(Guide)
void Circle(ImDrawList* dl, const ImVec2& c, float r, const char* label,
bool pressed)
{
dl->AddCircleFilled(c, r, pressed ? kLit : kFill);
dl->AddCircle(c, r, pressed ? IM_COL32(160, 210, 255, 255) : kEdge, 0,
1.5f);
const ImVec2 ts = ImGui::CalcTextSize(label);
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f), kTxt, label);
}
// Xbox 语义色面键
void Face(ImDrawList* dl, const ImVec2& c, float r, char label,
bool pressed)
{
ImU32 col;
switch (label)
{
case 'A': col = IM_COL32(32, 150, 40, 255); break;
case 'B': col = IM_COL32(200, 45, 45, 255); break;
case 'X': col = IM_COL32(50, 110, 220, 255); break;
default: col = IM_COL32(225, 185, 40, 255); break;
}
dl->AddCircleFilled(c, r, pressed ? col : IM_COL32(col & 0xFF,
(col >> 8) & 0xFF,
(col >> 16) & 0xFF,
60));
dl->AddCircle(c, r, pressed ? col : kEdge, 0, 2.0f);
const ImVec2 ts = ImGui::CalcTextSize(&label, &label + 1);
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f),
pressed ? IM_COL32(255, 255, 255, 255) : kTxt, &label);
}
// 摇杆画布(底座 + 死区圈 + 轴点)
void Stick(ImDrawList* dl, const ImVec2& c, float r, float ax, float ay,
float deadzone)
{
dl->AddCircleFilled(c, r, IM_COL32(32, 32, 37, 255));
dl->AddCircle(c, r, kEdge, 0, 1.5f);
dl->AddCircle(c, r * deadzone, IM_COL32(90, 140, 210, 110), 0, 1.2f);
dl->AddLine(ImVec2(c.x - r * 0.93f, c.y), ImVec2(c.x + r * 0.93f, c.y),
IM_COL32(50, 50, 58, 255), 1.0f);
dl->AddLine(ImVec2(c.x, c.y - r * 0.93f), ImVec2(c.x, c.y + r * 0.93f),
IM_COL32(50, 50, 58, 255), 1.0f);
const float mag = std::sqrt(ax * ax + ay * ay);
const float cl = (mag > 1.0f) ? 1.0f / mag : 1.0f;
const ImVec2 pt(c.x + ax * cl * (r - 8.0f),
c.y + ay * cl * (r - 8.0f));
if (mag > deadzone)
{
dl->AddCircleFilled(pt, 10.0f, IM_COL32(90, 170, 250, 70));
}
dl->AddCircleFilled(pt, 7.0f,
mag > deadzone ? kLit
: IM_COL32(140, 140, 148, 255));
}
// 十字键(真十字形,按下臂亮)
void Dpad(ImDrawList* dl, const ImVec2& c, float arm, float w,
const bool* b)
{
const ImVec2 l0(c.x - arm, c.y - w * 0.5f);
const ImVec2 l1(c.x, c.y + w * 0.5f);
const ImVec2 r0(c.x, c.y - w * 0.5f);
const ImVec2 r1(c.x + arm, c.y + w * 0.5f);
const ImVec2 u0(c.x - w * 0.5f, c.y - arm);
const ImVec2 u1(c.x + w * 0.5f, c.y);
const ImVec2 d0(c.x - w * 0.5f, c.y);
const ImVec2 d1(c.x + w * 0.5f, c.y + arm);
dl->AddRectFilled(l0, l1, b[kLeft] ? kLit : kFill, 4.0f);
dl->AddRectFilled(r0, r1, b[kRight] ? kLit : kFill, 4.0f);
dl->AddRectFilled(u0, u1, b[kUp] ? kLit : kFill, 4.0f);
dl->AddRectFilled(d0, d1, b[kDown] ? kLit : kFill, 4.0f);
dl->AddRect(l0, l1, kEdge, 4.0f);
dl->AddRect(r0, r1, kEdge, 4.0f);
dl->AddRect(u0, u1, kEdge, 4.0f);
dl->AddRect(d0, d1, kEdge, 4.0f);
}
// 线性扳机条(GLFW -1..1 → 0..1 填充)
void Trigger(ImDrawList* dl, const ImVec2& mn, const ImVec2& mx,
float raw)
{
dl->AddRectFilled(mn, mx, IM_COL32(32, 32, 37, 255), 4.0f);
const float t = (raw + 1.0f) * 0.5f;
if (t > 0.02f)
{
dl->AddRectFilled(mn, ImVec2(mn.x + (mx.x - mn.x) * t, mx.y),
IM_COL32(235, 120, 55, 255), 4.0f);
}
dl->AddRect(mn, mx, kEdge, 4.0f);
}
} // namespace
void GamepadPanel::Draw(const EditorContext& ctx)
{
if (!m_Open)
{
return;
}
if (!ImGui::Begin("Gamepad", &m_Open))
{
ImGui::End();
return;
}
Engine::GamepadState gp;
const bool connected =
ctx.app && ctx.app->GetWindow().PollGamepad(gp);
if (!connected)
{
ImGui::TextColored(ImVec4(0.60f, 0.60f, 0.60f, 1.0f),
"○ Not connected (JOYSTICK_1)");
ImGui::End();
return;
}
ImGui::TextColored(ImVec4(0.40f, 0.85f, 0.40f, 1.0f), "● Connected");
ImGui::Spacing();
// ---- 手柄画布(固定尺寸,元素 = 手柄真实位置;无背景板)----
constexpr float kW = 440.0f, kH = 262.0f;
constexpr float kDead = 0.15f; // = InputMap kRadialDeadZone
ImGui::BeginChild("pad", ImVec2(kW, kH), false);
ImDrawList* dl = ImGui::GetWindowDrawList();
const ImVec2 o = ImGui::GetWindowPos();
auto C = [&](float x, float y) { return ImVec2(o.x + x, o.y + y); };
// 顶部:线性扳机条 + 肩键
Trigger(dl, C(52, 8), C(122, 20), gp.axes[4]); // LT
Trigger(dl, C(kW - 122, 8), C(kW - 52, 20), gp.axes[5]); // RT
Block(dl, C(78, 40), 76.0f, 20.0f, "LB", gp.buttons[kLB]);
Block(dl, C(kW - 78, 40), 76.0f, 20.0f, "RB", gp.buttons[kRB]);
// 左上:左摇杆|左下:十字键(向右错开——真实手柄摇杆与十字不对齐)
Stick(dl, C(76, 118), 46.0f, gp.axes[0], gp.axes[1], kDead);
Dpad(dl, C(128, 210), 36.0f, 24.0f, gp.buttons);
// 中央:Back / Guide / Start
Block(dl, C(184, 74), 40.0f, 16.0f, "Back", gp.buttons[kBack]);
Circle(dl, C(220, 58), 13.0f, "G", gp.buttons[kGuide]);
Block(dl, C(256, 74), 40.0f, 16.0f, "Start", gp.buttons[kStart]);
// 右上:ABXY 菱形|右下:右摇杆(对称错开,向中心收)
const ImVec2 fc = C(360, 118);
Face(dl, ImVec2(fc.x, fc.y - 24), 17.0f, 'Y', gp.buttons[kY]);
Face(dl, ImVec2(fc.x - 24, fc.y), 17.0f, 'X', gp.buttons[kX]);
Face(dl, ImVec2(fc.x + 24, fc.y), 17.0f, 'B', gp.buttons[kB]);
Face(dl, ImVec2(fc.x, fc.y + 24), 17.0f, 'A', gp.buttons[kA]);
Stick(dl, C(312, 210), 46.0f, gp.axes[2], gp.axes[3], kDead);
ImGui::EndChild();
ImGui::End();
}
+21
View File
@@ -0,0 +1,21 @@
//
// GamepadPanel:手柄输入可视化回显(Tools 菜单弹出窗,非 dock 常驻)。
//
// 三块:摇杆画布(原始轴值 + 引擎死区参考圈)/ 按键网格(按下高亮)/
// 动作层回显(引擎死区+钳制后脚本真正拿到的 Move/Look/Jump)。
// 测试用途:不接手柄时显示未连接状态。
//
#ifndef HELLOVULKAN_GAMEPADPANEL_H
#define HELLOVULKAN_GAMEPADPANEL_H
#include "EditorContext.h"
class GamepadPanel {
public:
void Draw(const EditorContext& ctx);
bool m_Open = false; // Tools 菜单开关(窗口 X 也能关)
};
#endif //HELLOVULKAN_GAMEPADPANEL_H
+39 -10
View File
@@ -198,6 +198,12 @@ namespace
for (u32 i = 0; i < meshes.size(); ++i)
{
// 「mesh」下拉的过滤:选了具体子 mesh 就只列该槽位(与渲染/
// 物理的子引用语义一致);All meshes(-1)= 全列
if (mr.meshIndex >= 0 && static_cast<int>(i) != mr.meshIndex)
{
continue;
}
ImGui::PushID(static_cast<int>(i));
const std::string meshName =
@@ -689,13 +695,15 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::RadioButton("Plane", &shape, 1);
ImGui::SameLine();
ImGui::RadioButton("Trans.Plane", &shape, 2);
ImGui::SetNextItemWidth(-1.0f);
// 固定宽度而非 -1:弹窗是 AlwaysAutoResize,-1(撑满可用
// 宽度)会拿上一帧宽度当基准 → 逐帧膨胀直到撑满屏幕
ImGui::SetNextItemWidth(180.0f);
ImGui::SliderFloat("half size", &half, 0.01f, 10.0f,
"%.2f");
ImGui::ColorEdit3("color", col);
if (shape == 2)
{
ImGui::SetNextItemWidth(-1.0f);
ImGui::SetNextItemWidth(180.0f);
ImGui::SliderFloat("alpha", &alpha, 0.0f, 1.0f,
"%.2f");
}
@@ -1271,6 +1279,8 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::DragFloat("Radius", &cc->radius, 0.02f, 0.05f, 5.0f);
ImGui::DragFloat("Half Height", &cc->capsuleHalfHeight, 0.02f, 0.0f, 5.0f);
ImGui::DragFloat("Mass", &cc->mass, 1.0f, 1.0f, 500.0f, "%.0f");
// 形状附加偏移:叠加在脚底约定之上(模型 pivot 对齐用)
ImGui::DragFloat3("Shape Offset", &cc->shapeOffset.x, 0.02f, -5.0f, 5.0f);
// ---- Movement:扫掠运动学参数 ----
ImGui::SeparatorText("Movement");
ImGui::DragFloat("Max Slope (deg)", &cc->maxSlopeAngle, 1.0f, 1.0f, 89.0f);
@@ -1901,11 +1911,31 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
// 警告跳过)----
ImGui::SeparatorText("Clips");
const auto& files = AssetModelFiles();
std::vector<const char*> opts;
opts.push_back("<attach animation file...>");
// 显示真名(AssetDisplayName:文件 stem,导入资产库的
// 哈希 "asset" 读同目录 pathname 取原始名);重名者补
// 父目录区分——不再把哈希路径直接摆给用户(= 显示成 id)
std::vector<std::string> labels;
labels.reserve(files.size());
for (const auto& f : files)
{
opts.push_back(f.c_str());
std::string label = Engine::Model::AssetDisplayName(f);
if (std::find(labels.begin(), labels.end(), label) !=
labels.end())
{
label += " (" +
std::filesystem::path(f)
.parent_path()
.filename()
.string() +
")";
}
labels.push_back(std::move(label));
}
std::vector<const char*> opts;
opts.push_back("<attach animation file...>");
for (const auto& l : labels)
{
opts.push_back(l.c_str());
}
int sel = 0;
ImGui::SetNextItemWidth(-1.0f);
@@ -1925,7 +1955,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
}
// attach 遥测(骨骼匹配结果,面板下方显示)
s_LastAttachFile =
std::filesystem::path(path).filename().string();
Engine::Model::AssetDisplayName(path);
s_LastAttachMatched =
res.channelsMatched + res.channelsRetargeted;
s_LastAttachRetargeted = res.channelsRetargeted;
@@ -1938,10 +1968,9 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
for (size_t i = 0; i < anim->attachedAnimPaths.size(); ++i)
{
const auto& p = anim->attachedAnimPaths[i];
ImGui::BulletText("%s", std::filesystem::path(p)
.filename()
.string()
.c_str());
ImGui::BulletText("%s",
Engine::Model::AssetDisplayName(p)
.c_str());
ImGui::SameLine();
ImGui::PushID(static_cast<int>(i));
const bool doDetach = ImGui::SmallButton("x");
+152
View File
@@ -0,0 +1,152 @@
//
// LogPanel.cpp:运行时日志面板实现。
//
// 数据源 = Engine::Debug::LogRing(spdlog ring sink 落的整行)。
// 级别着色(trace 灰 / debug 青 / info 浅白 / warn 黄 / error 红 /
// fatal 亮红带背景条)+ 级别过滤 + 文本过滤 + Auto-scroll(滚动条
// 贴底才自动拉,上翻看历史不被拽回)。
//
#include "LogPanel.h"
#include <Engine/Core/LogRing.h>
#include <imgui.h>
#include <cctype>
#include <cstring>
#include <string>
void LogPanel::Draw(const EditorContext& ctx)
{
(void)ctx;
if (!ImGui::Begin("Log"))
{
ImGui::End();
return;
}
// ---- 工具栏:清空 / 自动滚动 / 文本过滤 / 级别过滤 ----
if (ImGui::Button("Clear"))
{
Engine::Debug::LogRing().Clear();
}
ImGui::SameLine();
ImGui::Checkbox("Auto-scroll", &m_AutoScroll);
ImGui::SameLine();
ImGui::SetNextItemWidth(160.0f);
ImGui::InputTextWithHint("##filter", "filter...", m_Filter, sizeof(m_Filter));
ImGui::SameLine();
ImGui::Checkbox("Trace", &m_ShowTrace);
ImGui::SameLine();
ImGui::Checkbox("Info", &m_ShowInfo);
ImGui::SameLine();
ImGui::Checkbox("Warn", &m_ShowWarn);
ImGui::SameLine();
ImGui::Checkbox("Error", &m_ShowError);
ImGui::Separator();
// ---- 版本感知:有新日志且滚动条贴底 → 拉底(上翻历史不拽回)----
const Engine::u64 ver = Engine::Debug::LogRing().Version();
bool scrollToBottom = false;
if (ver != m_LastVersion)
{
m_LastVersion = ver;
if (m_AutoScroll)
{
scrollToBottom = ImGui::GetScrollY() >=
ImGui::GetScrollMaxY() - 24.0f;
}
}
if (ImGui::BeginChild("loglist", {0.0f, 0.0f}, false,
ImGuiWindowFlags_HorizontalScrollbar))
{
const auto entries = Engine::Debug::LogRing().Snapshot();
for (const auto& e : entries)
{
using lv = spdlog::level::level_enum;
bool show = false;
switch (e.level)
{
case lv::trace:
case lv::debug: show = m_ShowTrace; break;
case lv::info: show = m_ShowInfo; break;
case lv::warn: show = m_ShowWarn; break;
case lv::err:
case lv::critical: show = m_ShowError; break;
default: show = m_ShowInfo; break;
}
if (!show) continue;
if (m_Filter[0] != '\0')
{
// 简单子串过滤(大小写不敏感)
std::string lower;
lower.reserve(e.msg.size());
for (char c : e.msg)
{
lower.push_back(static_cast<char>(
std::tolower(static_cast<unsigned char>(c))));
}
std::string needle;
needle.reserve(strlen(m_Filter));
for (const char* p = m_Filter; *p; ++p)
{
needle.push_back(static_cast<char>(
std::tolower(static_cast<unsigned char>(*p))));
}
if (lower.find(needle) == std::string::npos)
{
continue;
}
}
// ---- 渲染:级别徽标(恒彩色,固定列)+ 消息本体 ----
// 消息色只有 warn/error/fatal 染色(info 白是惯例),颜色
// 辨识主要靠徽标列——UE Output Log 的 Verbosity 列同位
ImVec4 tagCol, msgCol;
const char* tag = "INFO";
bool bg = false;
switch (e.level)
{
case lv::trace:
tag = "TRACE"; tagCol = {0.55f, 0.55f, 0.55f, 1.0f};
msgCol = {0.55f, 0.55f, 0.55f, 1.0f}; break;
case lv::debug:
tag = "DEBUG"; tagCol = {0.40f, 0.80f, 0.90f, 1.0f};
msgCol = {0.70f, 0.70f, 0.70f, 1.0f}; break;
case lv::info:
tag = "INFO"; tagCol = {0.45f, 0.72f, 0.95f, 1.0f};
msgCol = {0.90f, 0.90f, 0.90f, 1.0f}; break;
case lv::warn:
tag = "WARN"; tagCol = {1.00f, 0.78f, 0.20f, 1.0f};
msgCol = {1.00f, 0.78f, 0.20f, 1.0f}; break;
case lv::err:
tag = "ERROR"; tagCol = {1.00f, 0.35f, 0.35f, 1.0f};
msgCol = {1.00f, 0.35f, 0.35f, 1.0f}; break;
default:
tag = "FATAL"; tagCol = {1.00f, 0.15f, 0.15f, 1.0f};
msgCol = {1.00f, 0.15f, 0.15f, 1.0f};
bg = true; break;
}
if (bg)
{
// fatal:整行暗红背景条提级(先画背景再画文本)
const ImVec2 p0 = ImGui::GetCursorScreenPos();
const ImVec2 p1(p0.x + ImGui::GetContentRegionAvail().x,
p0.y + ImGui::GetTextLineHeight());
ImGui::GetWindowDrawList()->AddRectFilled(
p0, p1, IM_COL32(90, 10, 10, 160));
}
ImGui::TextColored(tagCol, "%s", tag);
ImGui::SameLine(40.0f); // 徽标固定列宽,消息对齐成两列
ImGui::TextColored(msgCol, "%s", e.msg.c_str());
}
if (scrollToBottom)
{
ImGui::SetScrollHereY(1.0f);
}
}
ImGui::EndChild();
ImGui::End();
}
+26
View File
@@ -0,0 +1,26 @@
//
// LogPanel:运行时日志面板(环形缓冲渲染,级别着色 + 过滤)。
//
#ifndef HELLOVULKAN_LOGPANEL_H
#define HELLOVULKAN_LOGPANEL_H
#include <Engine/Core/Types.h>
#include "EditorContext.h"
class LogPanel {
public:
void Draw(const EditorContext& ctx);
private:
bool m_ShowTrace = false;
bool m_ShowInfo = true;
bool m_ShowWarn = true;
bool m_ShowError = true;
bool m_AutoScroll = true;
char m_Filter[128] = "";
Engine::u64 m_LastVersion = 0;
};
#endif //HELLOVULKAN_LOGPANEL_H
+28
View File
@@ -173,6 +173,9 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
EditParam(
ImGui::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f));
EditParam(ImGui::ColorEdit4("Base Color", &p.baseColor.x));
// albedo 贴图 alpha 作细节叠加遮罩(脸部腮红/卧蚕等):rgb 叠加色,
// a 强度(0 = 关闭);需 albedo 贴图自带 alpha 遮罩才有局部效果
EditParam(ImGui::ColorEdit4("Alpha Overlay", &p.alphaOverlay.x));
EditParam(ImGui::DragFloat("IOR", &p.surface.x, 0.01f, 1.0f, 3.0f));
// 发光 = emissiveFactor × emissive 贴图 × Strength(Emissive 槽
// 兜底贴图恒黑,无贴图时颜色因子是唯一入口)
@@ -192,6 +195,23 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
ImGui::TextDisabled("Transmission/Thickness need Transparent");
}
// UV 镜像(材质级开关,作用于全部纹理槽):源资产 UV 朝向与引擎
// 上左原点约定相反时贴图整体翻转,这里一键纠正。写入 misc.y/z,
// 旧 .material 无该分量 → 缺省 0 = 关闭
{
bool flipU = p.misc.y > 0.5f;
const bool c = ImGui::Checkbox("Flip U (left-right)", &flipU);
if (c) { p.misc.y = flipU ? 1.0f : 0.0f; }
EditParam(c);
}
ImGui::SameLine();
{
bool flipV = p.misc.z > 0.5f;
const bool c = ImGui::Checkbox("Flip V (up-down)", &flipV);
if (c) { p.misc.z = flipV ? 1.0f : 0.0f; }
EditParam(c);
}
// ---- 纹理槽(变化 → 落盘 + waitIdle 重建)----
ImGui::SeparatorText("Textures");
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
@@ -216,6 +236,13 @@ void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
}
if (ImGui::BeginPopup("tex_pick"))
{
// 清除入口(Unity/UE 语义):该槽置空 → 回退兜底贴图
if (ImGui::Selectable("<none>"))
{
pendingSlot = static_cast<int>(s);
pendingPath.clear();
}
ImGui::Separator();
for (const auto& f : EditorMaterialUtil::AssetTextureFiles())
{
if (ImGui::Selectable(f.c_str()))
@@ -313,6 +340,7 @@ void MaterialEditorPanel::DrawBuiltinMode(const EditorContext& ctx)
ImGui::DragFloat("Metallic", &p.factors.x, 0.01f, 0.0f, 1.0f);
ImGui::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f);
ImGui::ColorEdit4("Base Color", &p.baseColor.x);
ImGui::ColorEdit4("Alpha Overlay", &p.alphaOverlay.x);
ImGui::DragFloat("IOR", &p.surface.x, 0.01f, 1.0f, 3.0f);
ImGui::ColorEdit3("Emissive Color", &p.emissiveFactor.x);
ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f, 0.0f, 100.0f);
+21
View File
@@ -9,6 +9,7 @@
#include "Engine/ECS/Components.h"
#include "Engine/Physics/PhysicsWorld.h"
#include "Engine/Core/Time.h"
#include "Engine/Core/Stats.h"
#include <imgui.h>
#include <glm/gtc/type_ptr.hpp>
@@ -208,6 +209,26 @@ void StatsPanel::Draw(const EditorContext& ctx)
{
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
}
// ---- GPU 资源台账(Stats 记账中枢的唯一消费者)----
const auto st = Engine::Stats::Get().TakeSnapshot();
auto MB = [](u64 bytes)
{ return static_cast<double>(bytes) / (1024.0 * 1024.0); };
ImGui::Separator();
ImGui::Text("textures: %llu (%.2f MB)",
static_cast<unsigned long long>(st.textureCount),
MB(st.textureBytes));
ImGui::Text("meshes: %llu (%.2f MB)",
static_cast<unsigned long long>(st.meshCount),
MB(st.meshBytes));
ImGui::Text("buffers: %llu (%.2f MB)",
static_cast<unsigned long long>(st.bufferCount),
MB(st.bufferBytes));
ImGui::Text("allocator (VMA 1:1): %.2f MB",
MB(st.memoryUsedBytes));
ImGui::Text("churn: %.2f MB / %.2f MB (total %.2f / %.2f)",
MB(st.frameAllocation), MB(st.frameDeallocation),
MB(st.totalAllocation), MB(st.totalDeallocation));
}
ImGui::End();
+8
View File
@@ -234,6 +234,14 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
// PIE 运行态:输入归游戏(脚本读 GLFW 轮询),Gizmo/编辑器
// 热键全部让位(UE Play 语义:Scene view 编辑操作冻结)
const bool playing = ctx.playState && *ctx.playState;
// PIE 启动瞬间:视口抢焦点(UE Play 语义——游戏视角接管)。
// 不抢的话焦点停在工具栏 Play 按钮上,编辑器相机飞行等
// 持焦操作下一局全失灵
static bool s_wasPlaying = false;
if (playing && !s_wasPlaying) {
ImGui::SetWindowFocus();
}
s_wasPlaying = playing;
if (!playing && out.focused && !ImGui::GetIO().WantTextInput &&
!ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
// Q = 纯选择模式(UE Q 工具语义):op=0 无任何轴位,