修复编辑器相机与 game 形态黑屏;中文字体与 Q 选择热键

FPS 式视角合成(yaw 世界轴/pitch 本地轴,永不 roll);
RMB 自由视角后环绕 pivot 失效待重建(修 Focus 后 MMB/滚轮
瞬移锁回);SceneRenderer 默认 kDisplayDirect(game 形态
Blit 空转+清屏黑屏根因);swapchain 优先 Opaque 合成;
ImGui 加载思源黑体(CJK 常用集);Q=纯选择不出杆;
新增 InputMap 动作映射
This commit is contained in:
2026-10-08 17:02:39 +08:00
parent 36b1fc0566
commit 26e222a9cf
15 changed files with 730 additions and 50 deletions
+3
View File
@@ -9,6 +9,7 @@
#define HELLOVULKAN_EDITORCONTEXT_H #define HELLOVULKAN_EDITORCONTEXT_H
#include <Engine/Core/Application.h> #include <Engine/Core/Application.h>
#include <Engine/Core/GameFlow.h>
#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>
@@ -28,6 +29,8 @@ 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;
// 游戏流状态机(PIE 状态权威;Editor::Init 注册快照/恢复监听)
Engine::GameFlow *flow = nullptr;
// 编辑器相机(Scene view 专用,归 Renderer 持有——与场景相机实体 // 编辑器相机(Scene view 专用,归 Renderer 持有——与场景相机实体
// 无关;面板经此做 F 焦点/Gizmo 视图/对齐跳转) // 无关;面板经此做 F 焦点/Gizmo 视图/对齐跳转)
Engine::EditorCamera *editorCamera = nullptr; Engine::EditorCamera *editorCamera = nullptr;
+11 -8
View File
@@ -220,12 +220,12 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
} }
} }
// ---- 工具热键(UE 语义):W/E/R 切 Gizmo 模式、T 切 // ---- 工具热键(UE 语义):Q 纯选择(不出杆)、W/E/R 切
// local/world、F 焦点选中实体。仅 Viewport 持焦时生效—— // Gizmo 模式、T 切 local/world、F 焦点选中实体。仅 Viewport
// 输入框打字(WantTextInput)由 BlockingKeyboard 拦游戏侧, // 持焦时生效——输入框打字(WantTextInput)由 BlockingKeyboard
// 但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按住 = 相机 // 拦游戏侧,但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按
// 飞行模式(键盘归相机),工具热键全部抑制——W 前进与 // 住 = 相机飞行模式(键盘归相机),工具热键全部抑制——W 前
// W 平移 Gizmo 的键位冲突就此解耦 ---- // 进与 W 平移 Gizmo 的键位冲突就此解耦 ----
auto &reg = ctx.scene->GetRegistry(); auto &reg = ctx.scene->GetRegistry();
ECS::TransformComponent *tr = ECS::TransformComponent *tr =
selected != entt::null selected != entt::null
@@ -236,6 +236,9 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
const bool playing = ctx.playState && *ctx.playState; const bool playing = ctx.playState && *ctx.playState;
if (!playing && out.focused && !ImGui::GetIO().WantTextInput && if (!playing && out.focused && !ImGui::GetIO().WantTextInput &&
!ImGui::IsMouseDown(ImGuiMouseButton_Right)) { !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {
// Q = 纯选择模式(UE Q 工具语义):op=0 无任何轴位,
// ImGuizmo 不绘制不响应——选中实体只高亮不出杆
if (ImGui::IsKeyPressed(ImGuiKey_Q)) gizmo.op = 0;
if (ImGui::IsKeyPressed(ImGuiKey_W)) gizmo.op = ImGuizmo::TRANSLATE; if (ImGui::IsKeyPressed(ImGuiKey_W)) gizmo.op = ImGuizmo::TRANSLATE;
if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE; if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE;
if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE; if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE;
@@ -370,8 +373,8 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
: "PLAYING - Ctrl+P to stop"); : "PLAYING - Ctrl+P to stop");
} else { } else {
std::snprintf(tip, sizeof(tip), std::snprintf(tip, sizeof(tip),
"Space: %s [T] Snap: Ctrl F: Focus " "Q/W/E/R: Sel/Move/Rot/Scale T: %s "
"Ctrl+Shift+F: To Selection", "Snap: Ctrl F: Focus",
gizmo.localSpace ? "Local" : "World"); gizmo.localSpace ? "Local" : "World");
} }
const ImVec2 imgMin = ImGui::GetItemRectMin(); const ImVec2 imgMin = ImGui::GetItemRectMin();
+2 -2
View File
@@ -29,8 +29,8 @@ struct ViewportInputState {
// op 取 ImGuizmo::OPERATION 位掩码(TRANSLATE=7=三轴位或运算,非 0 // op 取 ImGuizmo::OPERATION 位掩码(TRANSLATE=7=三轴位或运算,非 0
// 起始枚举;0 = 无任何轴位 → Gizmo 完全不绘制),int 存储避免本 // 起始枚举;0 = 无任何轴位 → Gizmo 完全不绘制),int 存储避免本
// 头文件 include ImGuizmo.h(拖入 imgui 宏依赖顺序) // 头文件 include ImGuizmo.h(拖入 imgui 宏依赖顺序)
// W/E/R 切换 op、T 切换 local/world 空间(UE 语义);dragging 拉起 // Q 纯选择(op=0 不出杆)、W/E/R 切换 op、T 切换 local/world 空间
// Editor::BlockingMouse 门控 // (UE 语义);dragging 拉起 Editor::BlockingMouse 门控
struct GizmoState { struct GizmoState {
int op = 7; // ImGuizmo::TRANSLATE(TRANSLATE_X|Y|Z) int op = 7; // ImGuizmo::TRANSLATE(TRANSLATE_X|Y|Z)
bool localSpace = true; // 操纵空间:true=模型轴(旋转/缩放语义), bool localSpace = true; // 操纵空间:true=模型轴(旋转/缩放语义),
+32
View File
@@ -18,6 +18,7 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include "Engine/Core/Types.h" #include "Engine/Core/Types.h"
#include "Engine/Input/InputAction.h"
namespace Engine { namespace Engine {
@@ -103,6 +104,37 @@ namespace Engine {
// 滚轮增量(上行 = +1,一档 ≈1):读后清零。UI 捕获鼠标 // 滚轮增量(上行 = +1,一档 ≈1):读后清零。UI 捕获鼠标
//(面板悬停)时不累积——滚轮归 ImGui //(面板悬停)时不累积——滚轮归 ImGui
f32 ConsumeScrollDelta(); f32 ConsumeScrollDelta();
// ---- 输入动作层(具名动作,键鼠+手柄混绑;InputMap 藏在 cpp)----
// 采样:主循环单点调用 Poll(物理/脚本/OnUpdate 之前),整帧
// 一致。active=false(GameFlow 非 Running)时全部动作静默清零。
// 默认绑定表在 Init 时内建(Move=Axis2D、Jump=Button)
// 装配期(编辑器/游戏配置阶段调用;运行中 Define 也可,索引重建)
void DefineAction(const std::string& name, ActionKind kind);
// 追加绑定:绑定到具名动作(动作不存在则忽略)
void BindAction(const std::string& name, const ActionBinding& b);
// 就地重绑定(bindingIdx = 该动作绑定表下标;下帧生效)
bool RebindAction(const std::string& name, size_t bindingIdx,
const ActionBinding& b);
// 持久化(yaml;Load 文件不存在静默失败保持现状表)
bool SaveActionMap(const std::string& path);
bool LoadActionMap(const std::string& path);
// 每帧采样(Application::Run 专用单点;上层勿调)
void Poll(bool active);
// 查询(按动作名;未定义动作返回 false/0)。Button 用
// Pressed/Down/Released 边沿语义,Axis 用 Value/Value2
bool ActionPressed(const std::string& name);
bool ActionDown(const std::string& name);
bool ActionReleased(const std::string& name);
f32 ActionValue(const std::string& name);
glm::vec2 ActionValue2(const std::string& name);
// 手柄连接态(Poll 时更新)
bool IsGamepadConnected();
} }
} // namespace Engine } // namespace Engine
+15
View File
@@ -17,6 +17,16 @@ struct GLFWwindow;
namespace Engine { namespace Engine {
// 手柄状态快照(POD,PollGamepad 每帧填充)。
// 值 = GLFW 标准游戏布局:axes 下标即 GLFW_GAMEPAD_AXIS_*(左摇杆 X/Y、
// 右摇杆 X/Y、LT、RT),buttons 下标即 GLFW_GAMEPAD_BUTTON_*(A=0…),
// 调用方不得依赖具体数值(GLFW 头语义)
struct GamepadState {
bool connected = false;
f32 axes[6] = {};
bool buttons[15] = {};
};
class Window { class Window {
public: public:
Window(const std::string& title, u32 width, u32 height); Window(const std::string& title, u32 width, u32 height);
@@ -76,6 +86,11 @@ namespace Engine {
void SetCursorCaptured(bool captured); void SetCursorCaptured(bool captured);
bool IsCursorCaptured() const { return m_CursorCaptured; } bool IsCursorCaptured() const { return m_CursorCaptured; }
// 手柄轮询(JOYSTICK_1 标准游戏布局):连接且带标准映射时填充
// state 并返回 true;否则置 connected=false 全零并返回 false。
// 无分配(GLFW 平台态拉取),每帧调用成本可忽略
bool PollGamepad(GamepadState& state) const;
private: private:
+70
View File
@@ -0,0 +1,70 @@
//
// InputAction.h:输入动作模型类型。
//
// 动作层把"游戏语义"(Move/Jump)与"物理输入源"(键/鼠标/手柄)解耦:
// - 动作(Action)= 具名游戏语义,Button(按下沿)/Axis1D/Axis2D
// - 绑定(Binding)= 一个输入源到动作贡献的映射(code = GLFW 标准码)
// 上层(脚本/玩法系统)只认动作名,换键位/换手柄不改代码。
//
// code 值约定 = GLFW 标准码(同 Input::Key 惯例:Key 即 glf3.h 键码,
// GamepadButton 即 GLFW_GAMEPAD_BUTTON_*,GamepadAxis 即
// GLFW_GAMEPAD_AXIS_*);调用方不得依赖具体数值。
//
// component:Axis2D 动作中该绑定贡献哪个分量(0=X,1=Y)。
// 例如 W → {Key, KEY_W, scale=-1, component=1}(Y 分量负方向 = 前进),
// 左摇杆 Y 轴 → {GamepadAxis, AXIS_LEFT_Y, scale=+1, component=1}
//(GLFW 摇杆推上为负,与 W 的 -1 语义自然对齐)。
//
#ifndef HELLOVULKAN_INPUTACTION_H
#define HELLOVULKAN_INPUTACTION_H
#include <glm/glm.hpp>
#include <string>
#include <vector>
#include "Engine/Core/Types.h"
namespace Engine {
namespace Input {
// 动作类型
enum class ActionKind : u8 {
Button, // 0/1 按下态(Down/Pressed/Released 边沿查询)
Axis1D, // 标量 [-1,1](键绑定 scale 加权、手柄轴原始值×scale)
Axis2D, // 平面向量(2D 径向死区 + 单位圆钳制)
};
// 绑定源类型
enum class SourceKind : u8 {
Key, // 键盘(code = GLFW 键码)
MouseButton, // 鼠标键(0=左 1=右 2=中)
GamepadButton, // 手柄按钮(0-14 标准布局)
GamepadAxis, // 手柄轴(0-5:左摇杆 X/Y、右摇杆 X/Y、LT、RT)
};
// 单条绑定:一个输入源对一个动作的贡献
struct ActionBinding {
SourceKind kind;
i32 code; // 见头注释(GLFW 标准码)
f32 scale; // 贡献权重(方向:键 ±1;轴 ±1)
u8 component = 0; // 仅 Axis2D 消费:0=X 分量,1=Y 分量
};
// 动作实例:定义 + 绑定表 + 双帧值(边沿派生用,见 InputMap)
struct InputAction {
std::string name;
ActionKind kind = ActionKind::Button;
std::vector<ActionBinding> bindings;
// prev/cur 双帧值(Button/Axis1D 用 x;Axis2D 用 xy)。
// Button 语义:Down = |cur.x|>0.5,Pressed/Released = cur/prev 边沿
glm::vec2 prev{0.0f, 0.0f};
glm::vec2 cur{0.0f, 0.0f};
};
} // namespace Input
} // namespace Engine
#endif //HELLOVULKAN_INPUTACTION_H
+88
View File
@@ -0,0 +1,88 @@
//
// InputMap.h:动作表(定义/绑定/采样/重绑定/持久化)。
//
// 装配期(DefineAction/Bind/SetDefaults/LoadYaml)与运行期(Poll + 查询)
// 分离:名字→下标哈希在 Define 期建好,Poll 只写值槽位——热路径零堆分配。
//
// 采样语义(Poll):
// - active=false(GameFlow 非 Running):全部动作值清零且 prev 同步清零,
// 恢复激活后按住的键产生一次 Pressed 边(符合游戏语义,无脏边)
// - active=true:键鼠经 Window 轮询(自带 UI 捕获门控),手柄经
// Window::PollGamepad;连接翻转打一条日志
// - Axis2D:各绑定按 component 分量加权求和 → 2D 径向死区(0.15) →
// 超出单位圆时归一化(斜向不超速)
//
#ifndef HELLOVULKAN_INPUTMAP_H
#define HELLOVULKAN_INPUTMAP_H
#include <glm/glm.hpp>
#include <string>
#include <unordered_map>
#include <vector>
#include "Engine/Core/Types.h"
#include "Engine/Input/InputAction.h"
namespace Engine {
class Window;
namespace Input {
class InputMap {
public:
// ---- 装配期 ----
// 定义动作(返回动作 id = 下标;重名忽略并返回已有 id)
size_t DefineAction(const std::string& name, ActionKind kind);
// 追加绑定(动作 id 非法时忽略)
void Bind(size_t id, ActionBinding b);
// 就地重绑定(下帧采样即生效)。成功返回 true
bool Rebind(size_t id, size_t bindingIdx, ActionBinding b);
// 内建默认表:Move(Axis2D)=WASD+方向键+左摇杆、Jump(Button)=Space+A
void SetDefaults();
// 清空全部动作与索引(LoadYaml 重建用)
void Clear();
// ---- 运行期 ----
// 每帧采样(主循环单点调用:物理/脚本/OnUpdate 之前)
void Poll(const Window& win, bool active);
bool Down(size_t id) const; // Button: 按住;Axis: |值|>0.5
bool Pressed(size_t id) const; // 本帧按下边沿
bool Released(size_t id) const; // 本帧松开边沿
f32 Value(size_t id) const; // Axis1D;Button 返回 0/1
glm::vec2 Value2(size_t id) const;// Axis2D;其他 kind 返回 (x,0)
// 手柄连接态(Poll 时更新;连接翻转打 INFO)
bool IsGamepadConnected() const { return m_GamepadConnected; }
// ---- 持久化(yaml)----
// 格式见 InputMap.cpp;保存失败(目录不存在等)返回 false
bool SaveYaml(const std::string& path) const;
// 文件不存在静默失败返回 false(保持现状表);存在则
// SetDefaults 后按 yaml 覆盖同名动作的绑定表
bool LoadYaml(const std::string& path);
// ---- 查询辅助 ----
size_t Count() const { return m_Actions.size(); }
// 名字→id;不存在返回 npos
static constexpr size_t npos = static_cast<size_t>(-1);
size_t Find(const std::string& name) const;
private:
void PollGamepadConnection(bool connected);
std::vector<InputAction> m_Actions;
std::unordered_map<std::string, size_t> m_ByName;
bool m_GamepadConnected = false;
};
} // namespace Input
} // namespace Engine
#endif //HELLOVULKAN_INPUTMAP_H
@@ -236,6 +236,7 @@ namespace Engine {
// UniformBuffer——PhysicsDebugPass::PrepareFrame 上传,pass 内绑定) // UniformBuffer——PhysicsDebugPass::PrepareFrame 上传,pass 内绑定)
Scope<UniformBuffer> m_PhysicsLineVB; Scope<UniformBuffer> m_PhysicsLineVB;
Scope<UniformBuffer> m_PhysicsTriVB; Scope<UniformBuffer> m_PhysicsTriVB;
Scope<UniformBuffer> m_DebugLineVB; // 通用调试源(Engine::Debug 服务)
// Editor gizmo 图元 ring VB(editor 专属,game 构建不分配): // Editor gizmo 图元 ring VB(editor 专属,game 构建不分配):
// 图标 billboard / 视锥线框 / 拾取代理壳三流——代理壳由 // 图标 billboard / 视锥线框 / 拾取代理壳三流——代理壳由
// HitProxyPass 经 AttachGizmoProxy 消费(同 VB 换 shader 重画) // HitProxyPass 经 AttachGizmoProxy 消费(同 VB 换 shader 重画)
@@ -205,7 +205,11 @@ namespace Engine {
bool m_BloomEnabled = true; bool m_BloomEnabled = true;
bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过) bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过)
int m_DebugView = 0; int m_DebugView = 0;
int m_DisplayMode = kDisplayViewport; // Editor 视口为默认形态 // 引擎默认 = 游戏直渲形态(与 Renderer::m_DesiredDisplayMode 的
// 默认一致)。编辑器形态在 App OnInit 显式切 Viewport——若这里
// 默认 Viewport,game 形态没人调 SetDisplayMode:Blit 恒空转 +
// ImGuiPass 清屏 → 纯黑屏(game 形态黑屏的根因)
int m_DisplayMode = kDisplayDirect;
bool m_GameView = false; // PIE 内嵌播放(Editor Play/Stop 驱动) bool m_GameView = false; // PIE 内嵌播放(Editor Play/Stop 驱动)
#ifdef HELLO_EDITOR #ifdef HELLO_EDITOR
+77 -1
View File
@@ -1,9 +1,10 @@
// //
// Input.cpp:统一输入门面实现(转发 Window 后端轮询接口)。 // Input.cpp:统一输入门面实现(转发 Window 后端轮询接口 + 动作层门面)。
// //
#include "Engine/Core/Input.h" #include "Engine/Core/Input.h"
#include "Engine/Core/Window.h" #include "Engine/Core/Window.h"
#include "Engine/Input/InputMap.h"
namespace Engine { namespace Engine {
namespace Input { namespace Input {
@@ -13,11 +14,15 @@ namespace Engine {
// 仅 Application::Run 期间被查询)。非 const:门面含 // 仅 Application::Run 期间被查询)。非 const:门面含
// 光标捕获等状态设置,不只查询 // 光标捕获等状态设置,不只查询
Window* s_Window = nullptr; Window* s_Window = nullptr;
// 动作表实例(Init 时建默认绑定:Move/Jump)
InputMap s_ActionMap;
} }
void Init(Window& window) void Init(Window& window)
{ {
s_Window = &window; s_Window = &window;
s_ActionMap.SetDefaults(); // 内建默认绑定表(LoadYaml 可覆盖)
} }
bool IsKeyDown(const Key key) bool IsKeyDown(const Key key)
@@ -60,5 +65,76 @@ namespace Engine {
return s_Window != nullptr ? s_Window->ConsumeScrollDelta() : 0.0f; return s_Window != nullptr ? s_Window->ConsumeScrollDelta() : 0.0f;
} }
// ---- 输入动作层 ----
void DefineAction(const std::string& name, ActionKind kind)
{
s_ActionMap.DefineAction(name, kind);
}
void BindAction(const std::string& name, const ActionBinding& b)
{
const size_t id = s_ActionMap.Find(name);
if (id != InputMap::npos) s_ActionMap.Bind(id, b);
}
bool RebindAction(const std::string& name, size_t bindingIdx,
const ActionBinding& b)
{
const size_t id = s_ActionMap.Find(name);
return id != InputMap::npos && s_ActionMap.Rebind(id, bindingIdx, b);
}
bool SaveActionMap(const std::string& path)
{
return s_ActionMap.SaveYaml(path);
}
bool LoadActionMap(const std::string& path)
{
return s_ActionMap.LoadYaml(path);
}
void Poll(const bool active)
{
if (s_Window != nullptr) s_ActionMap.Poll(*s_Window, active);
}
bool ActionPressed(const std::string& name)
{
const size_t id = s_ActionMap.Find(name);
return id != InputMap::npos && s_ActionMap.Pressed(id);
}
bool ActionDown(const std::string& name)
{
const size_t id = s_ActionMap.Find(name);
return id != InputMap::npos && s_ActionMap.Down(id);
}
bool ActionReleased(const std::string& name)
{
const size_t id = s_ActionMap.Find(name);
return id != InputMap::npos && s_ActionMap.Released(id);
}
f32 ActionValue(const std::string& name)
{
const size_t id = s_ActionMap.Find(name);
return id != InputMap::npos ? s_ActionMap.Value(id) : 0.0f;
}
glm::vec2 ActionValue2(const std::string& name)
{
const size_t id = s_ActionMap.Find(name);
return id != InputMap::npos ? s_ActionMap.Value2(id)
: glm::vec2{0.0f, 0.0f};
}
bool IsGamepadConnected()
{
return s_ActionMap.IsGamepadConnected();
}
} }
} // namespace Engine } // namespace Engine
+31
View File
@@ -254,4 +254,35 @@ namespace Engine
m_ScrollAccum = 0.0f; m_ScrollAccum = 0.0f;
return d; return d;
} }
bool Window::PollGamepad(GamepadState& state) const
{
GLFWgamepadstate gp;
if (glfwGetGamepadState(GLFW_JOYSTICK_1, &gp))
{
state.connected = true;
// 计数用 sizeof 推导(vendored GLFW 3.3 无 *_COUNT 常量),
// 拷贝上限取两侧较小值,防未来数组尺寸不一致越界
constexpr int kAxisCap = static_cast<int>(
sizeof(state.axes) / sizeof(state.axes[0]) <
sizeof(gp.axes) / sizeof(gp.axes[0])
? sizeof(state.axes) / sizeof(state.axes[0])
: sizeof(gp.axes) / sizeof(gp.axes[0]));
constexpr int kButtonCap = static_cast<int>(
sizeof(state.buttons) / sizeof(state.buttons[0]) <
sizeof(gp.buttons) / sizeof(gp.buttons[0])
? sizeof(state.buttons) / sizeof(state.buttons[0])
: sizeof(gp.buttons) / sizeof(gp.buttons[0]));
for (int i = 0; i < kAxisCap; ++i)
state.axes[i] = gp.axes[i];
for (int i = 0; i < kButtonCap; ++i)
state.buttons[i] = gp.buttons[i] == GLFW_PRESS;
return true;
}
// 未连接 / 无标准映射:全零(断开瞬间残留值清空,防卡键)
state.connected = false;
for (float& a : state.axes) a = 0.0f;
for (bool& b : state.buttons) b = false;
return false;
}
} // namespace Engine } // namespace Engine
+29 -19
View File
@@ -57,6 +57,16 @@ namespace Engine {
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // docking 分支 io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // docking 分支
// io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // 多视口 // io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; // 多视口
const char* cjkFont = "assets/fonts/SourceHanSansSC-Regular.otf";
if (io.Fonts->AddFontFromFileTTF(
cjkFont, 16.0f, nullptr,
io.Fonts->GetGlyphRangesChineseSimplifiedCommon()) != nullptr) {
ENGINE_INFO("ImGui font: {} (CJK common ranges)", cjkFont);
} else {
// 字体缺失不致命:图集退回内嵌英文默认字体(中文显示 ?)
ENGINE_WARN("ImGui CJK font not found: {} — fallback to default", cjkFont);
}
ImGui_ImplGlfw_InitForVulkan(window.GetHandle(), true); ImGui_ImplGlfw_InitForVulkan(window.GetHandle(), true);
ImGui_ImplVulkan_InitInfo info = {}; ImGui_ImplVulkan_InitInfo info = {};
@@ -132,25 +142,25 @@ namespace Engine {
} }
} }
#ifdef HELLO_EDITOR #ifdef HELLO_EDITOR
void ImGuiLayer::UpdatePreviewTexture(u32 slot, vk::ImageView view) void ImGuiLayer::UpdatePreviewTexture(u32 slot, vk::ImageView view)
{ {
if (slot >= kMaxFramesInFlight) return; if (slot >= kMaxFramesInFlight) return;
// 语义与 UpdateSceneTexture 完全一致(回收旧描述符 + 注册新 view) // 语义与 UpdateSceneTexture 完全一致(回收旧描述符 + 注册新 view)
if (m_PreviewTextures[slot]) { if (m_PreviewTextures[slot]) {
ImGui_ImplVulkan_RemoveTexture( ImGui_ImplVulkan_RemoveTexture(
(VkDescriptorSet)(intptr_t)m_PreviewTextures[slot]); (VkDescriptorSet)(intptr_t)m_PreviewTextures[slot]);
m_PreviewTextures[slot] = ImTextureID_Invalid; m_PreviewTextures[slot] = ImTextureID_Invalid;
} }
if (view) { if (view) {
m_PreviewTextures[slot] = (ImTextureID)(intptr_t) m_PreviewTextures[slot] = (ImTextureID)(intptr_t)
ImGui_ImplVulkan_AddTexture( ImGui_ImplVulkan_AddTexture(
(VkImageView)view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); (VkImageView)view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
} }
} }
#endif #endif
void ImGuiLayer::NewFrame() void ImGuiLayer::NewFrame()
{ {
ImGui_ImplVulkan_NewFrame(); ImGui_ImplVulkan_NewFrame();
+312
View File
@@ -0,0 +1,312 @@
//
// InputMap.cpp:动作表实现(采样 / 默认表 / yaml 持久化)。
//
#include "Engine/Input/InputMap.h"
#include <Engine/Core/Log.h>
#include <Engine/Core/Window.h>
#include <yaml-cpp/yaml.h>
#include <cmath>
#include <fstream>
namespace Engine {
namespace Input {
namespace {
// GLFW 标准码(仅默认表使用;与 Input::Key 值一致——键码即
// GLFW 物理键码,此处直接用数值避免 Core↔Input 头依赖)
constexpr i32 kKeyW = 87, kKeyA = 65, kKeyS = 83, kKeyD = 68;
constexpr i32 kKeyUp = 265, kKeyDown = 264,
kKeyLeft = 263, kKeyRight = 262;
constexpr i32 kKeySpace = 32;
// 手柄标准布局
constexpr i32 kPadBtnA = 0; // GLFW_GAMEPAD_BUTTON_A
constexpr i32 kPadAxisLeftX = 0; // GLFW_GAMEPAD_AXIS_LEFT_X
constexpr i32 kPadAxisLeftY = 1; // GLFW_GAMEPAD_AXIS_LEFT_Y
constexpr f32 kRadialDeadZone = 0.15f;
// 单绑定对一维贡献(Axis2D 的分量提取在 Poll 内单独做)
f32 SampleScalar(const Window& win, const GamepadState& pad,
const ActionBinding& b)
{
switch (b.kind) {
case SourceKind::Key:
return win.IsKeyPressed(b.code) ? b.scale : 0.0f;
case SourceKind::MouseButton:
return win.IsMouseButtonPressed(b.code) ? b.scale : 0.0f;
case SourceKind::GamepadButton:
return (pad.connected && pad.buttons[b.code])
? b.scale : 0.0f;
case SourceKind::GamepadAxis:
return pad.connected ? b.scale * pad.axes[b.code] : 0.0f;
}
return 0.0f;
}
// ---- yaml kind 字符串映射 ----
const char* KindStr(ActionKind k) {
switch (k) {
case ActionKind::Button: return "Button";
case ActionKind::Axis1D: return "Axis1D";
case ActionKind::Axis2D: return "Axis2D";
}
return "Button";
}
const char* SourceStr(SourceKind k) {
switch (k) {
case SourceKind::Key: return "Key";
case SourceKind::MouseButton: return "MouseButton";
case SourceKind::GamepadButton: return "GamepadButton";
case SourceKind::GamepadAxis: return "GamepadAxis";
}
return "Key";
}
ActionKind KindFromStr(const std::string& s) {
if (s == "Axis1D") return ActionKind::Axis1D;
if (s == "Axis2D") return ActionKind::Axis2D;
return ActionKind::Button;
}
SourceKind SourceFromStr(const std::string& s) {
if (s == "MouseButton") return SourceKind::MouseButton;
if (s == "GamepadButton") return SourceKind::GamepadButton;
if (s == "GamepadAxis") return SourceKind::GamepadAxis;
return SourceKind::Key;
}
} // anonymous namespace
// ---- 装配期 ----
size_t InputMap::DefineAction(const std::string& name, ActionKind kind)
{
if (const size_t exist = Find(name); exist != npos) {
ENGINE_WARN("InputMap: action '{}' already defined", name);
return exist;
}
InputAction a;
a.name = name;
a.kind = kind;
m_Actions.push_back(std::move(a));
const size_t id = m_Actions.size() - 1;
m_ByName[name] = id;
return id;
}
void InputMap::Bind(size_t id, ActionBinding b)
{
if (id >= m_Actions.size()) return;
m_Actions[id].bindings.push_back(b);
}
bool InputMap::Rebind(size_t id, size_t bindingIdx, ActionBinding b)
{
if (id >= m_Actions.size() || bindingIdx >= m_Actions[id].bindings.size())
return false;
m_Actions[id].bindings[bindingIdx] = b; // 就地改,下帧生效
return true;
}
void InputMap::Clear()
{
m_Actions.clear();
m_ByName.clear();
}
void InputMap::SetDefaults()
{
Clear();
constexpr u8 kX = 0, kY = 1;
// Move:WASD + 方向键 + 左摇杆(2D)
const size_t move = DefineAction("Move", ActionKind::Axis2D);
Bind(move, {SourceKind::Key, kKeyW, -1.0f, kY});
Bind(move, {SourceKind::Key, kKeyS, +1.0f, kY});
Bind(move, {SourceKind::Key, kKeyA, -1.0f, kX});
Bind(move, {SourceKind::Key, kKeyD, +1.0f, kX});
Bind(move, {SourceKind::Key, kKeyUp, -1.0f, kY});
Bind(move, {SourceKind::Key, kKeyDown, +1.0f, kY});
Bind(move, {SourceKind::Key, kKeyLeft, -1.0f, kX});
Bind(move, {SourceKind::Key, kKeyRight, +1.0f, kX});
Bind(move, {SourceKind::GamepadAxis, kPadAxisLeftX, +1.0f, kX});
Bind(move, {SourceKind::GamepadAxis, kPadAxisLeftY, +1.0f, kY});
// Jump:Space + 手柄 A
const size_t jump = DefineAction("Jump", ActionKind::Button);
Bind(jump, {SourceKind::Key, kKeySpace, 1.0f, kX});
Bind(jump, {SourceKind::GamepadButton, kPadBtnA, 1.0f, kX});
}
// ---- 运行期 ----
void InputMap::Poll(const Window& win, bool active)
{
if (!active) {
// 静默:值与 prev 同步清零——恢复后按住的键产生一次
// Pressed 边(prev=0 → cur!=0),无脏边
for (InputAction& a : m_Actions) {
a.prev = {0.0f, 0.0f};
a.cur = {0.0f, 0.0f};
}
PollGamepadConnection(false);
return;
}
// 手柄快照(栈上 POD,零堆分配)
GamepadState pad;
const bool connected = win.PollGamepad(pad);
PollGamepadConnection(connected);
for (InputAction& a : m_Actions) {
a.prev = a.cur;
glm::vec2 v{0.0f, 0.0f};
switch (a.kind) {
case ActionKind::Button:
case ActionKind::Axis1D:
for (const ActionBinding& b : a.bindings)
v.x += SampleScalar(win, pad, b);
break;
case ActionKind::Axis2D: {
for (const ActionBinding& b : a.bindings) {
const f32 s = SampleScalar(win, pad, b);
if (b.component == 0) v.x += s;
else v.y += s;
}
// 2D 径向死区 → 单位圆钳制(斜向不超速)
const f32 len = std::sqrt(v.x * v.x + v.y * v.y);
if (len < kRadialDeadZone) {
v = {0.0f, 0.0f};
} else if (len > 1.0f) {
v /= len;
}
break;
}
}
a.cur = v;
}
}
void InputMap::PollGamepadConnection(bool connected)
{
if (connected != m_GamepadConnected) {
m_GamepadConnected = connected;
ENGINE_INFO("InputMap: gamepad {}", connected ? "connected"
: "disconnected");
}
}
bool InputMap::Down(size_t id) const
{
if (id >= m_Actions.size()) return false;
const glm::vec2& c = m_Actions[id].cur;
return std::fabs(c.x) > 0.5f || std::fabs(c.y) > 0.5f;
}
bool InputMap::Pressed(size_t id) const
{
if (id >= m_Actions.size()) return false;
const InputAction& a = m_Actions[id];
return std::fabs(a.cur.x) > 0.5f && std::fabs(a.prev.x) <= 0.5f;
}
bool InputMap::Released(size_t id) const
{
if (id >= m_Actions.size()) return false;
const InputAction& a = m_Actions[id];
return std::fabs(a.cur.x) <= 0.5f && std::fabs(a.prev.x) > 0.5f;
}
f32 InputMap::Value(size_t id) const
{
if (id >= m_Actions.size()) return 0.0f;
return m_Actions[id].cur.x;
}
glm::vec2 InputMap::Value2(size_t id) const
{
if (id >= m_Actions.size()) return {0.0f, 0.0f};
return m_Actions[id].cur;
}
size_t InputMap::Find(const std::string& name) const
{
const auto it = m_ByName.find(name);
return it != m_ByName.end() ? it->second : npos;
}
// ---- 持久化 ----
bool InputMap::SaveYaml(const std::string& path) const
{
YAML::Emitter out;
out << YAML::BeginMap;
out << YAML::Key << "actions" << YAML::Value << YAML::BeginSeq;
for (const InputAction& a : m_Actions) {
out << YAML::BeginMap;
out << YAML::Key << "name" << YAML::Value << a.name;
out << YAML::Key << "kind" << YAML::Value << KindStr(a.kind);
out << YAML::Key << "bindings" << YAML::Value << YAML::BeginSeq;
for (const ActionBinding& b : a.bindings) {
out << YAML::Flow << YAML::BeginMap;
out << YAML::Key << "kind" << YAML::Value << SourceStr(b.kind);
out << YAML::Key << "code" << YAML::Value << b.code;
out << YAML::Key << "scale" << YAML::Value << b.scale;
out << YAML::Key << "component" << YAML::Value << b.component;
out << YAML::EndMap;
}
out << YAML::EndSeq;
out << YAML::EndMap;
}
out << YAML::EndSeq;
out << YAML::EndMap;
std::ofstream file(path);
if (!file.is_open()) {
ENGINE_ERROR("InputMap::SaveYaml: cannot open '{}'", path);
return false;
}
file << out.c_str();
return true;
}
bool InputMap::LoadYaml(const std::string& path)
{
YAML::Node root;
try {
root = YAML::LoadFile(path);
} catch (const YAML::Exception&) {
return false; // 文件不存在 / 格式坏:静默用现状表
}
// 基线 = 默认表(yaml 缺的动作保留默认绑定)
SetDefaults();
const YAML::Node actions = root["actions"];
if (!actions.IsSequence()) return true;
for (const YAML::Node& n : actions) {
const size_t id = Find(n["name"].as<std::string>(""));
if (id == npos) continue;
m_Actions[id].bindings.clear();
if (const YAML::Node& bs = n["bindings"]; bs.IsSequence()) {
for (const YAML::Node& bn : bs) {
ActionBinding b;
b.kind = SourceFromStr(bn["kind"].as<std::string>("Key"));
b.code = bn["code"].as<i32>(0);
b.scale = bn["scale"].as<f32>(1.0f);
b.component = bn["component"].as<u8>(0);
m_Actions[id].bindings.push_back(b);
}
}
}
ENGINE_INFO("InputMap: loaded {}", path);
return true;
}
} // namespace Input
} // namespace Engine
+38 -16
View File
@@ -40,20 +40,38 @@ namespace Engine {
{ {
if (wantLook) if (wantLook)
{ {
// ---- 6DOF 真自由视角:pitch/yaw 各绕本地轴右乘增量 // ---- FPS 式视角(UE/Unity 编辑器视口语义):
// 四元数(q = q·Δyaw·Δpitch),无欧拉读改写、无 // yaw 绕世界 up 左乘、pitch 绕本地 X 右乘——
// ±89° 钳制——可过顶/翻滚,存储层本就是四元数, // 右轴恒保持水平,水平线永不 roll。此前版本
// 不存在万向锁。符号沿用旧约定:鼠标右移向右看 // yaw 也绕本地轴(6DOF 自由视角),俯视后再
// (右手系绕 Y 正角向左 → yaw 取负)、鼠标下移 // 平移鼠标会带 roll,up 向量歪斜。pitch 用
// 低头(绕 X 正角抬头 → pitch 取负)---- // 前向量的仰角钳 ±89°:过顶会倒置视图,编辑
// 器视口不要这种行为。符号约定不变:鼠标右移
// 向右看、鼠标下移低头 ----
const glm::quat dyaw = glm::angleAxis( const glm::quat dyaw = glm::angleAxis(
glm::radians(-mouseDX * m_MouseSensitivity), glm::radians(-mouseDX * m_MouseSensitivity),
glm::vec3(0, 1, 0)); glm::vec3(0, 1, 0));
const glm::quat dpitch = glm::angleAxis(
glm::radians(-mouseDY * m_MouseSensitivity), const glm::vec3 fwd =
glm::vec3(1, 0, 0)); transform.GetRotationQuat() * glm::vec3(0, 0, -1);
const float pitch =
std::asin(glm::clamp(fwd.y, -1.0f, 1.0f));
float dPitch = glm::radians(-mouseDY * m_MouseSensitivity);
dPitch = glm::clamp(pitch + dPitch,
glm::radians(-89.0f),
glm::radians(89.0f)) - pitch;
const glm::quat dpitch =
glm::angleAxis(dPitch, glm::vec3(1, 0, 0));
transform.SetRotationQuat( transform.SetRotationQuat(
transform.GetRotationQuat() * dyaw * dpitch); dyaw * transform.GetRotationQuat() * dpitch);
// 自由视角改写朝向后,旧环绕 pivot 已不在视线上——
// 若保留,下一次 MMB/滚轮会绕老焦点猛拽位置(Focus
// 后飞远再 MMB 的"瞬移锁回":角增量 × 大半径 =
// 米级甩动)。采 Blender/UE 语义:pivot 跟随当前
// 视线,失效待 MMB/滚轮按新视线自动重建
m_HasPivot = false;
} }
else if (Input::IsKeyDown(Input::Key::LeftShift)) else if (Input::IsKeyDown(Input::Key::LeftShift))
{ {
@@ -116,12 +134,16 @@ namespace Engine {
if (wantLook || !flyRequiresRMB) if (wantLook || !flyRequiresRMB)
{ {
const f32 speed = m_MoveSpeed * deltaTime; const f32 speed = m_MoveSpeed * deltaTime;
if (Input::IsKeyDown(Input::Key::W)) { transform.SetPosition(transform.GetPosition() + forward * speed); } bool flew = false;
if (Input::IsKeyDown(Input::Key::S)) { transform.SetPosition(transform.GetPosition() - forward * speed); } if (Input::IsKeyDown(Input::Key::W)) { transform.SetPosition(transform.GetPosition() + forward * speed); flew = true; }
if (Input::IsKeyDown(Input::Key::A)) { transform.SetPosition(transform.GetPosition() - right * speed); } if (Input::IsKeyDown(Input::Key::S)) { transform.SetPosition(transform.GetPosition() - forward * speed); flew = true; }
if (Input::IsKeyDown(Input::Key::D)) { transform.SetPosition(transform.GetPosition() + right * speed); } if (Input::IsKeyDown(Input::Key::A)) { transform.SetPosition(transform.GetPosition() - right * speed); flew = true; }
if (Input::IsKeyDown(Input::Key::Space)) { transform.SetPosition(transform.GetPosition() + glm::vec3(0, speed, 0)); } if (Input::IsKeyDown(Input::Key::D)) { transform.SetPosition(transform.GetPosition() + right * speed); flew = true; }
if (Input::IsKeyDown(Input::Key::LeftShift)) { transform.SetPosition(transform.GetPosition() - glm::vec3(0, speed, 0)); } if (Input::IsKeyDown(Input::Key::Space)) { transform.SetPosition(transform.GetPosition() + glm::vec3(0, speed, 0)); flew = true; }
if (Input::IsKeyDown(Input::Key::LeftShift)) { transform.SetPosition(transform.GetPosition() - glm::vec3(0, speed, 0)); flew = true; }
// 飞行位移同样令环绕 pivot 失效(同自由视角:位移后老
// pivot 与视线脱钩,保留会让 MMB/滚轮绕老焦点拽位置)
if (flew) { m_HasPivot = false; }
} }
// ---- 滚轮推拉:有 pivot 时改环绕距离(指数步进,近处精细/ // ---- 滚轮推拉:有 pivot 时改环绕距离(指数步进,近处精细/
+16 -3
View File
@@ -24,6 +24,7 @@
#endif #endif
#include <Engine/Renderer/Passes/OutlinePass.h> #include <Engine/Renderer/Passes/OutlinePass.h>
#include <Engine/Physics/PhysicsDebugTypes.h> #include <Engine/Physics/PhysicsDebugTypes.h>
#include <Engine/Debug/DebugDraw.h>
#include <Engine/Renderer/Cubemap.h> #include <Engine/Renderer/Cubemap.h>
#include <Engine/Renderer/IblGenerator.h> #include <Engine/Renderer/IblGenerator.h>
#include <Engine/Renderer/TracyGpuProfiler.h> #include <Engine/Renderer/TracyGpuProfiler.h>
@@ -114,6 +115,11 @@ namespace Engine
u64(Passes::PhysicsDebugPass::kMaxTriVerts) * u64(Passes::PhysicsDebugPass::kMaxTriVerts) *
sizeof(PhysicsDebugVertex), sizeof(PhysicsDebugVertex),
kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer); kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer);
// 通用调试源 ring VB(Engine::Debug 服务;顶点布局与物理源一致)
m_DebugLineVB = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator,
u64(Engine::Debug::kMaxVerts) * sizeof(Engine::Debug::Vertex),
kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer);
#ifdef HELLO_EDITOR #ifdef HELLO_EDITOR
// 3.3 Editor gizmo 图元 ring VB(editor 专属;game 构建不分配)。 // 3.3 Editor gizmo 图元 ring VB(editor 专属;game 构建不分配)。
// eVertexBuffer 复用 UniformBuffer 的 per-slot fence 语义, // eVertexBuffer 复用 UniformBuffer 的 per-slot fence 语义,
@@ -502,7 +508,8 @@ namespace Engine
// TAA 之前)——调试图元进 TAA/Bloom/Tonemap 主链路 // TAA 之前)——调试图元进 TAA/Bloom/Tonemap 主链路
m_Passes.push_back(CreateScope<Passes::PhysicsDebugPass>( m_Passes.push_back(CreateScope<Passes::PhysicsDebugPass>(
*m_Device, *m_Allocator, *m_ShaderManager, *m_DescriptorManager, *m_Device, *m_Allocator, *m_ShaderManager, *m_DescriptorManager,
frame, m_HDRColor, m_Depth, *m_PhysicsLineVB, *m_PhysicsTriVB)); frame, m_HDRColor, m_Depth, *m_PhysicsLineVB, *m_PhysicsTriVB,
*m_DebugLineVB));
m_PhysicsDebugPass = m_PhysicsDebugPass =
static_cast<Passes::PhysicsDebugPass*>(m_Passes.back().get()); static_cast<Passes::PhysicsDebugPass*>(m_Passes.back().get());
#ifdef HELLO_EDITOR #ifdef HELLO_EDITOR
@@ -813,10 +820,16 @@ namespace Engine
// MDI 命令/count 同窗口落槽位 ring(fence 已等,写-读竞争关闭) // MDI 命令/count 同窗口落槽位 ring(fence 已等,写-读竞争关闭)
m_SceneGpu->UploadIndirect(frameSlot, frame); m_SceneGpu->UploadIndirect(frameSlot, frame);
// 物理调试图元同窗口上传(fence 已等);空帧计数归零,pass 零开销 // 物理调试图元同窗口上传(fence 已等);空帧计数归零,pass 零开销。
// 通用调试源(Engine::Debug)同窗口取走;EndFrame 翻页(清瞬时 +
// 清过期持久项)——即使物理源缺席也必须翻页,防瞬时缓冲累积
if (m_PhysicsDebug && m_PhysicsDebugPass) { if (m_PhysicsDebug && m_PhysicsDebugPass) {
m_PhysicsDebugPass->PrepareFrame(frameSlot, *m_PhysicsDebug); m_PhysicsDebugPass->PrepareFrame(
frameSlot, *m_PhysicsDebug,
Engine::Debug::IsEnabled() ? &Engine::Debug::Frame()
: nullptr);
} }
Engine::Debug::EndFrame();
#ifdef HELLO_EDITOR #ifdef HELLO_EDITOR
// Editor gizmo 图元同窗口上传(fence 已等);空帧不写,pass 端 // Editor gizmo 图元同窗口上传(fence 已等);空帧不写,pass 端