Compare commits

...
159 Commits
Author SHA1 Message Date
atdunbg 84cf547704 资产浏览器可视化改造 + 材质缩略图 GPU 烘焙 + 纹理上传批式化
- 资产浏览器可视化与配色:面板重构(网格/列表/缩略图)、AssetKind 分类、
  EditorColors 统一配色、AssetItemView 抽出条目绘制;HierarchyPanel/
  MaterialEditorPanel 同步调整
- 材质缩略图改走 GPU 回读烘焙:MaterialPreviewPass 新增 bake pass
  (SDR atlas → host staging copyImageToBuffer)、Renderer 提供
  Request/PollMaterialPreviewBake(无 waitIdle,依赖 per-slot fence),
  Editor 每帧排空后注入 ThumbnailCache——脱离预览 tile 池,消除"滚出视野
  即失效/池耗尽"两问题
- 缩略图失效:BakedIdentity(serial, editVersion) 与当前材质身份比对,
  换贴图槽(重载新 serial)/调参数(editVersion++)不符即 Invalidate 重烘,
  修"修改材质后资产浏览器缩略图不更新"
- 材质加载闸门:每帧至多 1 个 + 按实测耗时冷却,替代固定 2/帧
- 纹理上传批式化:VulkanAssetUploader 新增路径纹理缓存(跨材质去重,
  同一贴图只解码/上传一次)+ 同批新纹理合并为单 cmd/单 submit/单 fence;
  Texture 抽出 RecordPixelsUpload/CreateDeferred/ReleasePendingStaging,
  析构兜底释放 staging;MaterialAsset 纹理所有权移交 uploader 缓存——
  修大材质目录首次打开卡顿
2026-10-18 20:05:29 +08:00
atdunbg 2df6e57b61 透明分层屏幕空间折射 + 透明排序修正 + 默认清屏色归零
默认 ClearColor 0.1 → 0(策划阶段结束,无用)。

透明分层折射(玻璃见玻璃):透明段原来单个 ForwardTrans pass,折射
采样源 SceneColor 副本只在不透明段后快照一次(不含任何透明体)→ 前
玻璃折射不出后玻璃。改为按绘制单位远→近拆成至多 kTransparentLayers
层,每层绘制前刷新 SceneColor 副本,使层 L 玻璃采样含所有更远透明体。
RenderGraph pass 编译期定序,故层数固定;超出并入末层。深度 storeOp
改 eStore(多层需 Load 承接有效深度内容)。shader 不改。

透明排序修正:排序键由"相机到实例的欧氏距离"改为视图空间深度
(-(view·p).z)——欧氏距离把侧向偏移计入距离,偏侧物体被误判为更远
(正方体右上的甜甜圈在特定视角被排到后方)。并让透明队列逐实例成批
(不合并):透明本就是每层一个绘制单位,合并成多实例 batch 会让排序
只能用批平均深度、批内实例无法各自定序。零额外 draw 代价。
2026-10-18 17:09:42 +08:00
atdunbg f107854236 MeshFactory:程序化几何单一源 + CreatePrimitive 链路
新增 MeshFactory(Cube/Plane/Sphere/Cylinder/Cone/Torus + 切线计算),
Model 程序化几何改走 MeshFactory(收敛为单一几何源,消除重复的顶点
生成),并新增 CreatePrimitive;打通 AssetManager/IAssetUploader/
VulkanAssetUploader 的 CreatePrimitive 链路;编辑器 Procedural 弹窗
下拉 + ResolveProcedural 伪路径反解 + 场景序列化(程序化图元可存取)。
2026-10-18 17:09:25 +08:00
atdunbg 22309d24f0 材质与环境光照:UE 打包 Specular 导入(R=AO/G=Rough/B=Metal 与 glTF MR 同序 → 同一贴图直供 MetallicRoughness + Ao 槽,factors 直用贴图,默认置翻绿位 misc.w);MappedNormal 支持 DirectX 法线翻绿(misc.w,编辑器加 Flip Green 开关);环境遮蔽拆分漫反射与高光(高光遮蔽更狠:室内高光须压暗,此前表面反射天空贴图 = 室内穿帮);ufbx 导入日志补齐 mr/normal/ao/emissive 槽位 2026-10-18 15:23:07 +08:00
atdunbg e7711134fe 物理修复与相机防穿墙:Jolt Trace 接管(缺省 DummyTrace 带 JPH_ASSERT(false),调试版一被调用即断;同文去重防多线程刷屏);MeshShape 三角索引补基址(此前把 materialIndex/userData 当顶点偏移传 → 每个 mesh 三角全指向前一 mesh 顶点区,碰撞形状错位 + AABBTree 随机分割告警);调试绘制巨型网格三角预算(>5 万三角退化为包围盒线框,避免百万级 DrawLine 打到个位数帧率);角色 mEnhancedInternalEdgeRemoval 消网格地面 ghost contact 顶起/抖动;新增 RayCastStatic(只命中非移动层,碎块/动态物不当遮挡)→ ThirdPersonCamera 防穿墙拉近(瞬时拉近、平滑回弹、贴墙最小距离与回退余量可配) 2026-10-18 15:23:01 +08:00
atdunbg 2024772bac TAA 时域速度链路贯通:天空盒补写 velocity(此前恒 0,相机移动时天空/背景重投影错误 → 闪烁);skyPrevVP(上一帧未抖动 proj × 去平移视图)落帧;TAA 加去遮挡深度检测(重投影点历史深度与当前像素比对,新暴露区拒历史,需 near/far + 投影模式落帧);移除 min-depth dilation(把前景速度泄漏给轮廓旁背景像素 → 边界闪);Forward 不透明段 velocity 附件 Clear→Load 承接天空结果;velocity 不入抖动注释订正(抖动进 velocity = 静止画面持续亚纹素重采样发糊) 2026-10-18 15:22:48 +08:00
atdunbg beba491bcb 阶段性合并:动画重定向、碎裂共享库、第三人称玩法、编辑器面板与 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 的拒读)
2026-10-11 22:22:19 +08:00
atdunbg 334819bf97 修复打靶场射击链路:发射源/玩家控制走 primary 相机实体
- TestShoot 发射源从 EditorCamera(视口 UI 状态,PIE 渲染不用它)
  改为绑定实体位姿,挂 primary 相机实体即第一人称枪口;
  仅光标捕捉态开火,防编辑器交互误射
- 新增 PlayerController:FPS 控制(鼠标捕捉/Esc 释放/点视口重捕、
  yaw+pitch 转向、WASD+Space/Ctrl 移动+Shift 加速),驱动 primary
  相机实体 Transform,PIE 所见即玩家所见
- 靶子/碎片玻璃 alpha 0.42/0.45→0.85、自发光 2→3:低不透明度在
  亮背景下靶子几乎隐形(用户误认 HUD 横条为靶子)
2026-10-09 13:13:52 +08:00
atdunbg f15b104e77 60 秒限时打靶场玩法原型:碎裂逻辑提取 FractureLib 共享库(HostView 桥接 protected 底座)+ 玻璃靶 Target(跨脚本参数计分)+ GameDirector(靶阵/倒计时/重开/图元 HUD) 2026-10-09 13:00:18 +08:00
atdunbg 6854d53f11 per-mesh 引用:MeshRendererComponent.meshIndex(-1=整模型)贯穿渲染收集/物理 Mesh 碰撞(自动跟随)/场景序列化(缺省键向前兼容)/Inspector mesh 下拉 2026-10-09 12:38:05 +08:00
atdunbg 505217ff7e Spawn/Instantiate 选型约定落进基类与注册宏文档(判定问题+三条纪律;注册=Blueprintable 表态,UE 同题靠约定治理) 2026-10-09 12:31:33 +08:00
atdunbg f14aba4238 修自发光回归:glTF 导入端写 emissiveFactor(EMISSIVE_COLOR,贴图无 factor 时设白);Emissive 兜底纹理黑→白(黑兜底会把 factor/strength 一起乘没) 2026-10-09 00:12:35 +08:00
atdunbg 6447b5c0fa 材质表面参数接通:新增 emissiveFactor(无 emissive 贴图时的发光入口,glTF 语义)入 UBO/YAML/缓存键/两 shader;Transmission/Thickness 未勾 Transparent 时禁用编辑(不透明段 shader 不读) 2026-10-09 00:04:55 +08:00
atdunbg 235482733c 修渲染两处:透明段剔除背面(闭合体正反面乱序 over 混合=深度错乱,单面玻璃 UE/Unity 语义);prefilter mip0 128→256(roughness 0 镜面反射块状马赛克) 2026-10-08 23:50:11 +08:00
atdunbg c43adc339d 资产系统侧线第一波:ResolveProcedural 提为公共静态(键格式唯一定义点);Inspector 程序化模型入口;修 Prefab hover 误导性 not loaded;Content Browser 模型拖拽 → Outliner 创建实体 2026-10-08 23:36:08 +08:00
atdunbg 2cf8d6e076 修复碎片模型卸载的在飞 GPU 引用(vkDestroyBuffer in-use):门面批量卸载先 WaitDeviceIdle 再销毁(与 ReloadMaterialAsset 同一先例) 2026-10-08 23:23:15 +08:00
atdunbg affa1a7a56 碎片形状随机化:唯一代键替代命中点量化缓存键(每次命中重新随机 Voronoi),配套碎片模型生命周期回收(销毁次帧卸载 + OnDestroy 兜底) 2026-10-08 23:18:35 +08:00
atdunbg d4d37b4b28 修复 PIE 快照丢相机组件(序列化写出键 Attached ≠ 读侧 Camera)与子弹同帧多接触重复特效(Bullet 命中防重入) 2026-10-08 23:06:18 +08:00
atdunbg 21da38db92 WallFracture 碎片几何升级:网格方块 → 2D Voronoi 不规则凸棱柱(命中点周边种子加密,MeshCollider 凸包,运行时 ParsedModel 程序化资产) 2026-10-08 22:58:40 +08:00
atdunbg f20578bc4b TestShoot 弹头半径 0.08→0.12 反隧穿:离散检测下薄板碰撞窗口须大于每步位移(无 CCD) 2026-10-08 22:48:20 +08:00
atdunbg bca50976f3 WallFracture 碎裂手感修正:爆炸飞散 → 玻璃式碎裂(初速距离衰减 + 法线剥落外推 + 网格切细至 8×6) 2026-10-08 22:43:36 +08:00
atdunbg d2701a3d9b 新增程序化立方体与可碎墙验收:Model::CreateCube 五层链路 + WallFracture 碎片刚体群(命中碎裂/径向飞散/寿命清理) 2026-10-08 22:28:49 +08:00
atdunbg ed7cb417bd 新增 Prefab 系统:实体子树存取 + 编辑器 Save As Prefab/拖入实例化 + 脚本 Instantiate 2026-10-08 22:01:26 +08:00
atdunbg b89f431896 新增脚本实体底座与碰撞事件:Spawn/Destroy + OnCollisionEnter/Exit + TestShoot 验收脚本
- ScriptBehavior::Spawn/Destroy:Scene::CreateEntity / 延迟销毁队列直达(实现下沉 ScriptRuntime.cpp,避 Application 不完整类型)
- ScriptRuntime::DispatchCollisions:帧首把 PhysicsWorld::GetEvents 派发到涉事实例(Persisted 跳过、禁用不收;回调内 spawn/destroy 安全)
- TestShoot:左键 spawn 动态球刚体子弹,OnCollisionEnter 命中点生成 ImpactFx 特效实体并自毁,超时防泄漏
2026-10-08 21:45:10 +08:00
atdunbg 26e222a9cf 修复编辑器相机与 game 形态黑屏;中文字体与 Q 选择热键
FPS 式视角合成(yaw 世界轴/pitch 本地轴,永不 roll);
RMB 自由视角后环绕 pivot 失效待重建(修 Focus 后 MMB/滚轮
瞬移锁回);SceneRenderer 默认 kDisplayDirect(game 形态
Blit 空转+清屏黑屏根因);swapchain 优先 Opaque 合成;
ImGui 加载思源黑体(CJK 常用集);Q=纯选择不出杆;
新增 InputMap 动作映射
2026-10-08 17:02:39 +08:00
atdunbg 36b1fc0566 新增调试绘制体系:DebugDraw 服务 + 物理调试 pass + 脚本侧力学线
通用图元(线/箭头四翼头/球/胶囊/盒)瞬时+持久双缓冲聚合;
物理 pass 画刚体速度/重力/接触反力;角色 collider 与力学
线归位(引擎只画资产、脚本画行为);线宽按设备上限钳制
2026-10-08 17:02:27 +08:00
atdunbg ff955b9e63 新增脚本参数系统:声明式参数 + Inspector 自动控件 + 场景序列化
DeclareParams/ParamBuilder 声明字段,运行时反射读写;
Inspector 按类型生成控件,SceneSerializer 直通存取;
动画系统接入参数化裁剪;新增 OrbitMove 示例脚本
2026-10-08 17:02:19 +08:00
atdunbg f327f44269 新增 GameFlow 运行时状态机:PIE 与独立运行统一生命周期
Stopped→Loading→Running↔Paused 单一状态源,编辑器 Play/Stop
与 main.cpp 独立运行共用同一套加载/恢复/暂停语义
2026-10-08 17:02:09 +08:00
atdunbg f73e52926f Reorganize directory layout: editor panels/, grouped shaders, drop stale header
- App/editor: move 7 panels into panels/ (Editor core stays at root)
- shaders/: group into pbr/ post/ debug/ env/ (runtime load paths updated)
- Delete outdated duplicate Engine/src/Renderer/VulkanAssetUploader.h
2026-10-07 09:52:17 +08:00
atdunbg cc837bc605 Add animation controller system, material assets and skeleton visualization
- .animctrl state machine asset (params/transitions/blend trees, 1D+2D
  directional blending) with Unity Animator-style editor panel: state
  list, transitions, params, directional pad preview driving live
  CrossFade on the selected entity
- Split animation evaluation out of SceneRenderer into
  AnimationSystem/AnimationEvaluator; skin palettes with rest-pose
  fallback and blend-weight epsilon filtering
- MaterialAsset (.material) with reload-safe preview tile pool
  (DropMaterial clears dirty refs before instance destruction)
- EditorGizmoPass skeleton wires + viewport joint overlay; camera
  projection rework (baked Vulkan Y-flip, ortho support)
- Editor/game build split via HELLOVULKAN_EDITOR option
2026-10-07 01:50:04 +08:00
atdunbg a941207ae3 Raise bloom threshold to HDR 3.0; fix blit dst-mip layout warnings
The smooth brass case back of the pocket watch reflects large bright
sky regions of the Venice dawn environment at HDR values well above 1.
With the old threshold of 1.0 the entire metal surface passed the
bloom extract, and the blurred additive result washed it out to flat
white. Only genuinely hot highlights (HDR >= 3, near-white after ACES)
should bloom.

- Default bloomThreshold 1.0 -> 3.0; Stats slider range extended to 8
  with a logarithmic scale
- Texture mip generation: transition the whole mip chain to
  TRANSFER_DST up front; blitting into never-transitioned destination
  mips triggered one validation warning per level
2026-10-05 12:28:14 +08:00
atdunbg 6c80693618 Fix bloom glow flicker on static geometry under TAA jitter
TAA subpixel jitter makes rasterized HDR specular highlights oscillate
per frame. The old RGB min/max neighborhood clamp re-clamped the
converged history against jitter-driven extremes every frame, leaving a
periodic residual that the bloom threshold then amplified into visible
glow pulsing on static models.

- TAA: replace the RGB AABB clamp with YCoCg variance clipping
  (history clamped toward the stable neighborhood mean +/- 1.25 sigma,
  intersected with the min/max box as a ghosting upper bound)
- Bloom extract: 5-tap cross prefilter before the threshold spreads
  surviving 1px jitter energy over a ~4x4 footprint so the glow no
  longer pulses
2026-10-05 12:08:53 +08:00
atdunbg 5d88ed2057 Improve PBR quality: mip chains, anisotropic filtering, baseColorFactor
Root causes behind blurry/unphysical materials (4K pocket watch render
mushy, glass fully opaque, watch hands unreadable):

- Textures loaded via stbi had a single mip level: 4096x4096 albedo/MR
  maps sampled with no LOD -> minification aliasing turned fine detail
  (watch hands) into noise. Generate a full blit-based mip chain with
  per-level layout transitions.
- samplerAnisotropy was never enabled on the device and every sampler
  was created with anisotropy disabled: grazing-angle surfaces (watch
  face, ground) collapsed into a blur. Enable the device feature and
  16x anisotropic filtering on material texture samplers.
- glTF baseColorFactor was ignored: the glass material (alphaMode
  BLEND, no baseColor texture, baseColorFactor.a = 0.3) fell back to
  the default white texture with alpha = 1 and rendered fully opaque.
  MaterialParams gains a baseColor vec4 (rgb tints albedo, a scales
  the blend alpha) threaded through both PBR shaders.
- alphaMode MASK was treated as opaque: add a factors.w switch that
  performs an alpha cutout test in the opaque shader.
- Fix material cache key dropping factors.z (separate-roughness
  switch); it now encodes the full factors and baseColor vectors.
2026-10-05 12:01:07 +08:00
atdunbg a3037cd401 Input facade: add cursor capture, mouse delta and scroll delta
SetCursorCaptured / IsCursorCaptured / GetMouseDelta / ConsumeScrollDelta
now route through the Input facade, so the editor camera no longer takes
a Window& at all: Camera::Update and EditorCamera::Update lose their
window parameter and SandboxApp calls Update(dt) directly. App layer
now has zero Window/GLFW usage for input - everything goes through
Engine::Input.
2026-10-05 11:45:57 +08:00
atdunbg 641b255cb2 Unify input behind Engine::Input facade; fix Ctrl+P and Pause button
New Engine/Core/Input.h/.cpp: static input facade with a Key/MouseButton
enum (values map to GLFW key codes internally) so scripts and game logic
no longer touch GLFW directly - window backend becomes swappable and the
facade is the surface to expose when embedding scripting later. Wired up
in Application's constructor; Camera (editor camera WASD/RMB/MMB), the
WASDMove script and SandboxApp key handling now query Input only.

Ctrl+P hotkey fix: the !m_Playing guard made the hotkey dead exactly
when it was needed - after pressing Play. Now toggles both ways.

Toolbar Pause button now reads "Resume" while paused (tooltip follows).
2026-10-05 11:34:27 +08:00
atdunbg c083ee5cb7 Fix PIE deadlock when playing a scene without a primary camera
BuildFrame returned false (skip frame) when the game view had no
primary camera, skipping the WaitFence/ResetFence/submit chain - the
frame-slot fence never got signaled, so the next WaitFence on that
slot blocked forever. Reproduced instantly by playing an empty scene
with embedded PIE; latent in manual fullscreen preview too.

Game view now falls back to the editor camera instead of skipping the
frame, keeping the per-slot fence cadence intact. Viewport warning
text updated to mention the editor-view fallback.
2026-10-05 11:21:50 +08:00
atdunbg 1a278d5945 PIE: viewport-embedded playback with UE-style main toolbar
Play no longer switches to fullscreen direct rendering: PIE now keeps
the editor UI alive and renders the primary camera into the Viewport
panel (new SceneRenderer::SetGameView flag; kDisplayDirect stays a
manual fullscreen preview for later release form). Editor camera stops
being driven during PIE, and viewport click-pick is gated off while
playing (clicks are game input now).

The Play/Stop buttons move out of the menu bar into a dedicated main
toolbar row below it (centered, UE Main Toolbar semantics): green Play
when idle; Pause (yellow, highlighted while paused) + Stop (orange)
while playing. Ctrl+P hotkey kept. Stats "Pause Simulation" merged
into the toolbar Pause; viewport overlay shows PAUSED accordingly.
2026-10-05 11:14:24 +08:00
atdunbg 5ca5ac2a1e Editor: group Details parameters and reorganize Stats panel
Details: multi-parameter components (RigidBody/Joint/CharacterController/
Animation) now use SeparatorText category headers in UE Details style;
all parameter labels normalized to Pascal Case.

Stats: rearranged into View / Scene / Rendering / Physics / Performance
collapsing groups. Clear Color moved to Scene, Debug View to Rendering,
GPU Timings + Frame merged into Performance; bare top-level widgets and
the now-obsolete Environment group removed.
2026-10-05 01:20:00 +08:00
atdunbg 432089747f Environment settings become scene entities (UE Skybox/SkyLight semantics)
Skybox/SkyLight are now Actor-carried components instead of resident
global settings, matching UE where SkyAtmosphere/SkyLight/fog are
Actors added from Place Actors and configured in Details.

- New SkyboxComponent (active + envPath asset slot) and SkyLightComponent
  (active + ambientColor); first-active entity wins per frame
- SceneRenderer collects env from components into FrameSceneData
  (DrawSkybox/envPath/ambient); Renderer performs delta sync - env
  reloads only when the resolved path changes, neutral fallback when
  empty or unloadable
- App-side env plumbing removed: kEnvPaths table and getEnvIndex/
  setEnvIndex callbacks deleted; StatsPanel keeps ClearColor + debug
  view, env params move to Details
- Serializer: scene-level Settings stores ClearColor only; env
  components serialize per entity; legacy v1 scenes (Environment header
  + Settings.AmbientColor/DrawSkybox) auto-migrate into an Environment
  entity on load for visual parity
- Inspector: Skybox/Sky Light sections with .hdr asset slot dropdown
  and Add Component entries; Outliner "+ Create" popup offers
  Empty/Light/Camera/Environment
2026-10-05 01:02:21 +08:00
atdunbg b7d6f53591 Editor: rework default layout to UE5 arrangement
Panels renamed to UE semantics (docking identity change): Hierarchy ->
Outliner, Inspector -> Details, Asset Browser -> Content Browser.

Default DockBuilder layout now matches UE5 factory layout: Details
docked right (full height), Content Browser docked bottom spanning
left+center, Outliner top-left, Viewport center with Stats as a
side tab.

Legacy imgui.ini (old window names, no new names) is removed once at
Init so the new default rebuilds cleanly; fresh or new-style ini is
untouched.
2026-10-05 00:47:33 +08:00
atdunbg 42757bab36 AssetManager: drop immediate SaveGUIDRegistry on registration
2268fa5 added the dirty-flag deferred flush but left the immediate
save in place, so new GUID registrations wrote the registry twice
(once inline, once at the next FlushGUIDRegistry). Remove the inline
save so the deferred path is the only writer.
2026-10-05 00:39:48 +08:00
atdunbg dec171cd1e Fix selection outline never showing for the first entity
entt entity 0's integral bits are 0, colliding with the "no selection"
sentinel: u32 selected == 0 meant "clear selection", so the scene's
first-created entity could never be outlined. Replaced with
std::optional<u32> + FrameSceneData::selectedEntityValid.

Also restore per-batch instance contiguity broken by the batchMats
flattening (e038cb3): entities sharing a mesh scatter across the flat
instance array in collection order. ViewScratch now tracks per-batch
singly-linked lists and appendView aggregates each batch into a
contiguous range, preserving the flat-array performance intent.

Hierarchy click selection now routes through SelectEntity so outline
stays in sync when picking from the panel.
2026-10-05 00:39:48 +08:00
atdunbg e038cb39fa SceneRenderer 实例收集扁平化:batchMats 从 vector<vector> 改为单 vector 按 (first,count) 切片,优化大场景下批次实例存储与 cache 命中率 2026-10-04 21:26:48 +08:00
atdunbg 3e85285e41 修改SceneRenderer 的存储的世界矩阵存储方式 2026-10-04 20:10:49 +08:00
atdunbg 2268fa5f76 资产改为保存场景或者关闭软件时候更新guid文件,vulkan/DescriptorManager 创建DescriptorPool时候注释eFreeDescriptorSet,PhysicsWorld初始化时候改为推荐的inNumBodyMutexes 2026-10-04 19:55:49 +08:00
atdunbg c1854453e5 修改场景实体查询为unordered_set存储,提升查询效率, 修改Physics的SyncScene也为unordered_set 2026-10-04 19:12:53 +08:00
atdunbg b78d9fb4c9 修改错误的TryBitsByBodyId返回值 2026-10-04 18:49:18 +08:00
atdunbg c8a1cd90bf 修复physics物理数据计算不正确的问题,同时为物理添加map索引 2026-10-04 18:42:23 +08:00
atdunbg ee5ab8e94e feat: parent-child transform hierarchy (UE Actor attach semantics)
- TransformComponent TRS = local (relative to parent); cached world SRT
  composed hierarchically (parent pos/rot/scale, no matrix decomposition
  shear under non-uniform parent scale)
- HierarchyComponent (parent + children, lazily attached); Scene::
  SetParent (keepWorld, cycle-safe) / GetParent / IsDescendantOf /
  SetWorldPose (world->local inverse, used by physics writeback & gizmo)
- UpdateTransforms: DFS from roots, recompose only dirty-or-dirty-
  ancestor subtrees (parents before children)
- DestroyEntity: descendants buried with parent (UE cascade)
- Serialization v2: optional Parent uuid key, two-pass relink on load
- Hierarchy panel: tree view, drag-drop reparent / drop-to-root detach,
  Create Child / Detach context menu; Inspector shows parent name
- Consumers moved to world-space reads: lights, camera pos, physics
  bodies/characters/joints/collider baking, gizmo, F-focus, AlignTo
2026-10-04 15:22:25 +08:00
atdunbg 97408e506c fix: ImGui hover jumps while cursor captured (RMB look / MMB orbit)
With GLFW_CURSOR_DISABLED the official GLFW backend keeps feeding
virtual unbounded cursor coords to ImGui each frame, so hover state
wanders across hidden panels while the physical cursor is invisible.
Append a "mouse left the window" pos event before ImGui::NewFrame
whenever the cursor is captured - ImGui treats the frame as mouseless.
2026-10-04 15:00:29 +08:00
atdunbg 230df67db7 fix: editor camera draw distance 100m -> 10km (UE semantics)
UE's far clip is effectively unlimited and shadow distance is an
independent parameter. Split kCameraFar (main projection, 10km) from
kShadowDistance (cascade splits, unchanged 100m) so distant geometry
is no longer clipped while shadow quality stays bit-identical.
Main projection now references the shared constants instead of
duplicated literals.
2026-10-04 14:49:27 +08:00
atdunbg 13055cc8ac fix: assimp INCOMPLETE flag no longer rejects animation-only FBX
Assimp flags every mesh-less scene as AI_SCENE_FLAGS_INCOMPLETE
(FBXConverter), so pure animation FBX never reached clip parsing -
the scene was discarded before ParseAnimations ran. Only treat null
scene / missing root node as hard failure; log INCOMPLETE as a
warning and continue. Final validity stays with ParsedModel::Valid().
2026-10-04 09:31:23 +08:00
atdunbg 175ce35def fix: animation-only FBX assets loadable; clip data viewer
Pure-animation FBX (no meshes, skeleton clips only) was rejected at
the cache layer: ParsedModel::Valid() required non-empty meshes, so
Model::FromParsed/LoadModel returned null and the asset never made it
into the cache. Animation assets are now first-class:

- ParsedModel::Valid() = meshes or clips non-empty; parse failures
  (both empty) still rejected. Empty-mesh consumers already guarded
  (rendering skips empty mesh loop, mesh collider falls back to Box)
- ParseFromDisk: fix null-deref when assimp returns a null scene
- AssetCache::Find(path) + AssetManager::GetModelByPath for panels
- Asset Browser hover card shows meshes/bones/clips for loaded models
- Inspector model line flags "animation-only" when meshes == 0
- Animation section: collapsible clip data table (name/duration/
  channel count) to verify bone-name matching of attached clips
2026-10-04 02:31:30 +08:00
atdunbg 2577809ee4 feat: Play-In-Editor (PIE) with in-memory scene snapshot
Editor toolbar gains Play/Stop (Ctrl+P) with UE semantics:

- Play: serialize current scene to an in-memory YAML snapshot via new
  SceneSerializer::SerializeToString, switch display to kDisplayDirect
  (primary camera entity renders), unfreeze time (timeScale 0 -> 1)
- Stop: DeserializeFromString atomically rebuilds the pre-play scene,
  back to kDisplayViewport (editor camera), refreeze time; selection
  cleared and gizmo drag reset since all entity handles dangle
- SceneSerializer refactored: SerializeDoc/ParseRoot shared by file
  and memory variants; file Save/Load behavior unchanged
- Running state disables gizmo + editor hotkeys in the viewport (input
  goes to game scripts), draws yellow PLAYING hint and a no-camera
  warning; File->Save/Load disabled while playing (UE behavior)

Physics bodies/constraints/characters and script instances are
reconciled automatically by the existing per-frame sync on restore.
2026-10-04 02:22:49 +08:00
atdunbg c949e8684b build: strip Jolt debug symbols from Release (55MB -> 11.3MB exe)
Jolt's GENERATE_DEBUG_SYMBOLS option defaults to ON and injects -g in
every config, so the Release binary carried ~44MB of DWARF sections
(.debug_info/.debug_line/...) from Jolt TUs alone -- 80% of the 55MB
exe. Force the option OFF for Release builds only (the vendored
submodule is untouched; Debug/RelWithDebInfo keep symbols).

Measured: 55.0MB -> 11.3MB, .text unchanged at 8.5MB, zero debug
sections left. The remaining ~1.6MB is the ELF symtab, useful for
crash stacks; plain strip takes it to 9.7MB if a distributable is
needed.
2026-10-04 02:10:08 +08:00
atdunbg 90097a756e fix(editor): rotation drag accumulates without wrap (UE session semantics)
Dragging rotation hit a wall at the wrap point: each frame re-read
euler from the quat and wrapped to [0,360), so dragging past 360
snapped the display back to 0 (and backwards dragging wrapped up to
360) -- values "jumped around" instead of accumulating.

Port UE TransformDetails session semantics: the rotation row shows a
local mirror value while the user edits it (accumulating freely in
both directions, never wrapping), and re-syncs from the quat only on
foreign changes (gizmo drag, physics writeback, entity switch).
Detection is exact component-wise quat equality: the mirror quat comes
from the same QuatFromEuler used by SetRotation, so an untouched pair
compares bit-equal, while any foreign writeback -- including sub-0.05
degree gizmo steps that a dot-product threshold would miss -- differs
in at least one float. Edit frames skip the check (mirror leads the
quat by one drag delta), gated on the previous frame's item-active
state.
2026-10-04 02:00:42 +08:00
atdunbg 4498bc7535 feat(editor): rotation display in UE's 0-360 convention
User pointed out the real gap: UE Details shows rotation in [0,360)
(-90 reads 270); our readback exposed [-180,180] with negative zeros.
GetRotation now wraps each axis to [0,360) via fmod -- strictly
equivalent math (each axis has 360-degree period), so matrix-rebuild
consumers like Camera::EulerRotMat are unaffected. The one consumer
needing a continuous domain is the free-look pitch clamp [-89,89] in
Camera::Update, which wraps back to [-180,180) before applying deltas.
2026-10-04 01:46:05 +08:00
atdunbg 4fc48c21ac fix(editor): euler display precision at gimbal lock (90 -> 89.98)
Typing 90 degrees on the pitch axis showed 89.98. The stored quat is
exact (matrices differ by 0.0) -- the noise is display-side: quat's
sin(90) components square to 0.99999994 in float, and asin() diverges
at |sin|->1, amplifying 6e-8 into 0.02 degrees. Yaw/roll use atan2 and
were always exact, matching the user's report being X-axis only.

Two fixes, both aligned with how UE gets away with it (internally UE
is quat + equally pathological, its Details panel just shows one
decimal):
- GetRotation gimbal-lock threshold 1.0 -> 1-1e-6: pitch near +-90
  now takes the composite-angle atan2 branch (ratio-stable) instead
  of asin, so typing 90.0 reads back exactly 90.0. Probed: X=90 and
  X=-90 now round-trip to 0 error; generic poses stay ~1e-6 deg.
- Rotation row shows one decimal ("%.1f", UE Details default), masking
  the residual asin noise for angles near (not at) the pole; Ctrl+Click
  input stays full precision. Negative zero from atan2 cleaned up.

Remaining asin noise for e.g. 89.9 (~9e-4 deg) is below the 1-decimal
display and unreachable in practice.
2026-10-04 01:37:49 +08:00
atdunbg 90bf63e641 feat(animation): attach external animation files to a model
Animation-only FBX files (no mesh, clips only) could not be used:
clips are keyed by node index into the parsed model's node table, so
there was no path to get them onto a different model's skeleton.

Add AssetManager::AttachAnimationClips: parses the container with the
CPU-only path (no GPU upload -- empty meshes cannot build a Model),
remaps each clip's channels to the target model's node indices by
bone name, and appends to its clip list. Channels whose bone name is
absent are skipped and counted; clips with zero matching channels are
dropped. Same-source attach is idempotent via a new
AnimationClip::sourcePath marker; name clashes get the file stem
prefixed.

Inspector Animation block gains an attach slot (UE Details semantics)
listing model-container files plus a bullet list of attached sources;
already-attached paths are replayed idempotently when the entity is
selected, so clips survive restarts. The paths persist in the scene
(Animation.Attached); the clips themselves stay runtime state.

Limitations (documented, by design): same-skeleton retargeting only
-- bone names must match between source and target (same DCC export
or Mixamo-style per-animation downloads); cross-skeleton retargeting
is out of scope.
2026-10-04 01:25:04 +08:00
atdunbg 69671cf4eb feat(editor): manual mesh renderer setup + asset hover info card
Mesh Renderer was missing from the Add Component menu and its model
reference was read-only, so a mesh could only be attached by loading
a saved scene. Add it to the menu (Unity Add Component semantics) and
give the block a UE-Details-style asset slot: a dropdown listing all
model files under assets/, selecting applies LoadModel (cache deduped).

Asset browser files now show a UE Content Browser style hover card:
kind, size, modified time, and loaded state (new AssetCache::Contains
query, read-only -- hovering never triggers loading).

Fix latent FATALs unlocked by manual assignment: AssetCache::Get
aborts on invalid handles, so rendering, animation sampling, and mesh
collider build now guard mr.model.Valid() before calling it.
2026-10-04 01:07:38 +08:00
atdunbg 7d02d344e5 fix(scripts): prevent duplicate script attachment per entity
The per-entity script list allowed picking the same script class
twice; the name-keyed runtime silently ignores duplicates, leaving
the extra rows dead weight with unusable enable bits. Enforce
DisallowMultipleComponent semantics: the dropdown of each row only
offers unattached names (own name kept for echo), "+ Add Script"
disables when every registered script is attached, and scene loading
dedupes as a safety net for hand-edited files.
2026-10-04 00:57:45 +08:00
atdunbg fd2d8f9b34 fix(editor): asset browser directory navigation
The file list was hardwired to the root directory, so it was always
empty (all assets live in subdirectories). Directories are now
selectable from both the tree (click row) and the list (click [D]
row), with a breadcrumb + Root button. Prefix check done manually
(path::starts_with is missing in this libstdc++).
2026-10-04 00:55:19 +08:00
atdunbg ab7a43a686 feat(editor): multi-script entities + asset browser panel
ScriptComponent now holds a list of {name, active} items (Unity
semantics: one entity, many scripts, each with its own enable bit;
component-level active stays as the master switch). ScriptRuntime
reconciles instances by name per entity -- rename/remove destroys
only the affected instances, untouched ones keep ticking. Scenes
persist Script.Items; old single-script key still loads.

AssetBrowserPanel (lightweight Unity Project window): fixed roots
(assets/scenes) as tabs, recursive directory tree, file list with
type filter (models/textures/scenes) and name substring filter,
double-click preloads models/HDRs into the asset caches (path-
deduped). Scene files point to the File menu for now; drag & drop
is a later iteration. Docked with Stats by default in fresh layouts.
2026-10-04 00:43:06 +08:00
atdunbg 74a2044441 chore(scripts): rename script API to GetComponent/TryGetComponent; misc
Clearer accessor naming on ScriptBehavior (Get -> GetComponent,
TryGet -> TryGetComponent), WASDMove follows. Teleport marked const
(JPH Ref does not propagate constness). Spinner: auto-rotation
disabled for the moment, registration switched from the registration
macro to an explicit lambda to verify the mechanism.
2026-10-04 00:33:47 +08:00
atdunbg 134812dae3 feat(physics): debug-draw character controller capsule
CharacterVirtual is not a registered body, so PhysicsSystem::DrawBodies
never renders it (Jolt provides no CharacterVirtual::DebugDraw either).
Add CharacterController::DebugDraw (shape wireframe at foot-origin +
shapeOffset, green) and drive it from CollectDebugData under the
colliders bit.
2026-10-03 19:01:57 +08:00
atdunbg 29c413579d refactor(physics): rename isSensor to isTrigger (Unity/UE terminology)
UE calls it Trigger (Overlap Events / Trigger preset) and Unity's UI
says "Is Trigger"; "sensor" is the physics-backend term (Jolt sensor,
Box2D sensor). Rename the component/record flag and the Inspector
label. Scene files now persist "IsTrigger" with backward-compatible
read of the old "IsSensor" key.
2026-10-03 18:53:38 +08:00
atdunbg b6d0868e15 feat(ecs): per-component enable/disable + inspector rework
Add `active` flag to the 8 behavior components (Unity enabled / UE
SetActive semantics; structural Id/Name/Transform excluded). Consumers
skip disabled components: renderer (mesh/light/anim), primary camera
election, physics reconciliation (body/constraint/character destroyed
and rebuilt on toggle), scripts keep instance but stop ticking.

Inspector: component headers now carry an enable checkbox and a remove
button, Add-Component menu greys out attached entries, and conflict
hints added (RigidBody <-> CharacterController mutual exclusion, Joint
requires RigidBody, Animation requires MeshRenderer, unregistered
script name). Scenes persist Active per component (default true).
2026-10-03 18:45:03 +08:00
atdunbg cad0481bc0 feat(app): WASDMove script (WASD camera-relative movement)
ScriptBehavior gains App() host access (Application injected via
ScriptRuntime), TryGet<T> and EntityBits for physics facade addressing.
WASDMove maps WASD to the editor camera's horizontal heading (UE
gameplay convention): dynamic bodies go through the physics velocity
facade preserving the vertical component (zeroing Y would cancel
gravity mid-fall), bodiless entities translate their transform directly.
Editor-mode key conflict is naturally avoided: RMB-held flight consumes
WASD, release hands keys to the script.
2026-10-03 18:27:12 +08:00
atdunbg 3403822a52 feat(engine): C++ native script system (ScriptBehavior + registry)
ScriptComponent stores only a script name string (POD, serializable);
instances are reconciled by ScriptRuntime each frame (PhysicsWorld
pattern): orphan/rename cleanup with OnDestroy, per-name instantiation
from a file-level self-registering registry (ENGINE_REGISTER_SCRIPT),
OnCreate on bind, OnUpdate after physics step on scaled dt. Engine
framerate loop only sees the base interface, so future Lua/C# behaviors
plug in as foreign ScriptBehavior implementations with zero engine
changes. Ships a Spinner sample script and Inspector script picker.
2026-10-03 18:02:49 +08:00
atdunbg ea6beee593 fix(physics): editor drag teleports dynamic bodies
SyncScene only reconciled static/kinematic transforms into Jolt bodies;
dragging a dynamic entity (esp. while paused, where Writeback never runs)
moved the visual but left the body -- and the collider debug wireframe --
at the old pose. Dynamic bodies now get the same drag reconciliation
(teleport + zero velocity + activate), and Writeback syncs
lastPos/lastRot to the body pose so physics motion is never misread as
editor dragging (which would teleport the body back).
2026-10-03 17:51:11 +08:00
atdunbg d5a86c170e fix(renderer): mesh collider source positions in render space for skinned meshes
Skinned meshes keep vertices in raw mesh space (bakeTransform=false);
rendering transforms them via bind palette (global(bone) x invBind, folds
FBX unit scale). Physics MeshShape/ConvexHull and the mesh bounding sphere
consumed the raw positions directly -> M1911 hull measured ~10x the visual
size. ParseFromDisk now bakes bind-palette-skinned positions per vertex
(same formula as the skinning shader) into ParsedMesh::cpuPositions; Mesh
uses them for m_CpuPositions and bounds. GPU vertex data and rendering are
untouched.
2026-10-03 17:43:55 +08:00
atdunbg e1cef53158 physics: character controller (Jolt CharacterVirtual)
- CharacterControllerComponent: capsule params (radius/halfHeight, mass,
  max slope, step height); entity origin = feet via shape offset
- CharacterController per-entity wrapper: gravity integration + grounded
  clamp, ExtendedUpdate (sweep/slide + stick-to-floor + walk-stairs),
  SetLinearVelocity/Jump/IsGrounded facade
- PhysicsWorld: SyncCharacters reconcile (create/rebuild on param
  change, editor-drag teleport), UpdateCharacters after physics Update
  (Jolt recommended order), writes back to Transform
- Inspector: params + grounded/velocity telemetry + jump debug button
- Serialization: params only, state rebuilt by reconcile
2026-10-03 16:34:20 +08:00
atdunbg 82910c41e8 physics: joint constraints (fixed/hinge/ball)
- JointComponent: type, target UUID, per-body local anchors, hinge axis
  + angle limits (data only, zero Jolt deps)
- PhysicsWorld::SyncConstraints: create/remove/rebuild on snapshot
  change; body rebuild contagion via invalidated-bits list; target
  resolved lazily by UUID each sync (no two-pass load needed)
- Jolt FixedConstraint/HingeConstraint/PointConstraint in
  LocalToBodyCOM space (anchors minus shape COM); fixed frames lock
  creation-time relative pose; hinge axis aligned across bodies
- Inspector: Joint section with target entity name dropdown
- Serialization: Joint map with UUID target, as<T> fallbacks
2026-10-03 16:26:20 +08:00
atdunbg a744b332eb physics: capsule/cylinder/compound collider shapes
- ColliderType + Capsule/Cylinder/Compound; CompoundChild list (Box/
  Sphere/Capsule, local pos/quat, non-recursive)
- CreateBody: CapsuleShape/CylinderShape (ctor param orders differ) +
  StaticCompoundShape; empty/failed compound falls back to Box
- BodyRecord caches radius/halfHeight/compound; SyncScene rebuilds on
  change (per-collider param extraction)
- Inspector: combo entries + shape params + compound child list editor
- Serialization: new fields with as<T> fallback, compound seq round-trip
2026-10-03 16:17:44 +08:00
atdunbg 6c81bfd70d physics: rigid body drive API (force/impulse/torque/velocity)
PhysicsWorld facade methods addressed by entity bits via body records:
AddForce/AddImpulse/AddTorque (dynamic bodies only, auto-wake), Set/
GetLinearVelocity/SetAngularVelocity (dynamic + kinematic). Inspector
RigidBody block gains impulse debug controls for manual response tests.
Transient by design — nothing serialized.
2026-10-03 16:10:22 +08:00
atdunbg fae6614e70 editor camera: dedicated Scene-view camera decoupled from camera entities
EditorCamera (UE FEditorViewportClient semantics) owned by SceneRenderer:
own TransformComponent + reused Camera controller. BuildFrame dispatches
render source by display mode — editor form always renders from the
editor camera (usable in empty scenes), game form renders from the
scene's primary camera entity. AlignTo copies an entity pose into the
editor camera ("Align Viewport to Camera" button in Inspector). F focus
and viewport gizmo now follow the same dispatch. main.cpp stripped of
dangling sandbox camera/spinner state; onSceneLoaded hook removed.
2026-10-03 15:52:28 +08:00
atdunbg e2a9c21e68 fix(serialization): load failure on hardened yaml-cpp + flow-style arrays
yaml-cpp master hardens zombie nodes: operator=, size(), Type() and
chained operator[] all throw InvalidNode instead of silently passing.
Storing per-component nodes via Node::operator= threw for every key
that was absent, so all 7 entities were skipped on load (0 loaded).
Reads are now level-by-level guarded (copy-init + IsDefined), which
only uses the safe surface (as<T>(fallback), begin(), IsDefined).

Save side switched from node-tree streaming to a manual Emitter:
vector/quat fields emit as single-line flow arrays ([0, 1.2, -6])
and model refs as inline maps, with 9-digit float precision for
lossless float32 round-trips.

Verified offline against the previously saved scene: 7/7 entities
parse with correct component counts; emitter output round-trips.
2026-10-03 15:30:36 +08:00
atdunbg 007d175314 serialization: scene save/load (Unity-style YAML + UE-style GUID registry)
Engine-layer SceneSerializer (yaml-cpp submodule): entities/components
to versioned YAML, atomic clear-and-rebuild on load with per-entity
fault tolerance, missing-field defaults, unknown-component skip and
UUID dedup. Asset references are {Guid, Path} pairs resolved through
the new AssetManager GUID registry (assets/asset_registry.yaml) with
path fallback; procedural:// pseudo-paths rebuild from parameters.
Editor File menu gets Save/Load Scene modals; app re-resolves stale
entity handles via onSceneLoaded callback.
2026-10-03 14:47:02 +08:00
atdunbg ffe33e9967 camera: UE Scene-view semantics — RMB fly, MMB focus-orbit, wheel dolly, F focus
Key conflict fix (UE semantics): while RMB is held the keyboard belongs
to the camera, so WASD no longer collides with the gizmo W/E/R tool
hotkeys. Editor mode gates fly movement behind RMB; game mode keeps
always-on WASD. Viewport tool hotkeys (W/E/R/T/F) are suppressed while
RMB is held.

Focus orbit (pivot): MMB drag rotates the camera around a pivot keeping
distance; first orbit picks a pivot 5m ahead on the view ray. Shift+MMB
pans in screen plane (camera and pivot move together). Mouse wheel
dollies toward/away from the pivot with exponential steps (no pivot:
move along view ray). F focuses the selected entity (frame-selection):
pivot set to its position, camera slides along the view ray; orbit
distance persists.

Window gains a polled scroll-delta channel (ConsumeScrollDelta) that
respects ImGui mouse capture; Camera::Update mutates orbit state (no
longer const).
2026-10-03 13:02:55 +08:00
atdunbg 9ff08651f0 editor: revert self-drawn scale gizmo, clip degenerate axes instead
User feedback: the self-drawn scale handles clash visually with the
ImGuizmo translate/rotate rods (different size/style, clearly not one
widget). Keep the library widget for all three ops for visual
consistency and prevent mis-drags by masking degenerate axes via
SetAxisMask:

- translate: axis ~ parallel to camera-to-object line (sin < 0.25)
- scale: axis ~ perpendicular to camera-to-object line (|dot| < 0.1,
  intersection numerator identically zero -> grab fails/values jump)
  OR axis ~ parallel to view direction (|dot| > 0.98, zero screen
  displacement -> value never changes)
- rotate: screen-space math, no degeneracy, no mask

Masked axes are neither drawn nor activatable; a slight view change
restores them. Center uniform-scale square (MT_SCALE_XYZ, pure screen
space) stays available in any view.
2026-10-03 12:46:07 +08:00
atdunbg c9d4eed043 editor: scale gizmo fully self-drawn (screen-space, view-independent)
ImGuizmo scale hover/drag relies on ray-plane intersection with the
axis as plane normal; in front views (axis ⟂ camera-to-object) the
intersection numerator is identically zero, so handles can't be
grabbed and values jump randomly; axes parallel to the view have zero
screen displacement. Submodule is read-only and AxisMask can only hide
degenerate axes, not replace them.

Replace the library scale path entirely with a screen-space gizmo:
- three axis rods (line + end square) + center uniform-scale square,
  constant screen size in any view
- axis drag: mouse delta projected onto the axis' screen direction
- axis pointing at camera: radial mapping (drag away = grow)
- center square: uniform scale (library MT_SCALE_XYZ semantics)
- Ctrl snapping quantizes the result; scale clamped >= 0.01
- pick suppressed while hovering/dragging the self-drawn handles
2026-10-03 12:23:00 +08:00
atdunbg 21b33d7d7f fix(editor): 缩放轴病态判据与平移互补,按 op 分派 AxisMask
缩放独有老问题根因(源码实证):缩放 hover(GetScaleType)与拖拽
(HandleScale ratio 分母)依赖"法线=轴"平面求交,交点距离分子 numer
= -dist·dot(camToObj, 轴)。正对视角相机与物体无横偏时 numer≡0 →
len=0/垃圾 → 杆 hover 不到、ratio 分子分母乱跳(数值随机猛变)。
平移 hover 是纯屏幕距离、平面法线经正交化自适应;旋转是屏幕角数学
——故唯独缩放有此病。SCALE op 下判据换 |dot(camToObj,轴)|<0.1,
ROTATE 不 mask,TRANSLATE 保持 sinθ<0.25。中心块等比缩放
(MT_SCALE_XYZ,屏幕空间数学)不受 mask 恒可用。
2026-10-03 12:11:16 +08:00
atdunbg a130850276 feat(editor): Gizmo 退化轴自动隐藏(调用层 SetAxisMask,UE 语义)
正对视角 + 物体竖直偏移时,单轴拖拽灵敏度 sinθ(轴与相机连线夹角)→0:
拖拽投影系数趋 0("数值不变"),激活平面与鼠标射线共面(IntersectRayPlane
denom→0 返回 -1 被当交点距离)。按灵敏度 <0.25(θ<14.5°)将病态轴
SetAxisMask 隐藏(库公开 API,按轴关绘制+激活,保留含该轴的平面手柄
与中心块兜底),相机移动后自动恢复——可激活性决定可见性,零库改动。
2026-10-03 12:03:07 +08:00
atdunbg 1d360369c9 revert: 删除 SetPlaneLimit 覆盖,Gizmo 参数与 Horizon 对齐(全默认)
对照 Horizon 实现(同官方 ImGuizmo submodule、零定制、Manipulate 全
默认参数)确认库默认行为就是可用的出货行为。0.034 把轴隐藏阈值从
11.5° 抬到 20°,常规视角轴被过早隐藏("箭头又没了");之前 0.35 同
根因。两次阈值折腾全部推翻:自行覆盖库内部可见度参数本身就是错的方向。
2026-10-03 02:27:46 +08:00
atdunbg af241857af fix: 轴可见度阈值 0.35→0.034(量纲修正,0.35 会隐藏所有轴)
前次 0.35 的错误:把阈值当成 0~1 的 sinθ 直接比较。实际库内 gizmo
恒定屏幕尺寸(mGizmoSizeClipSpace=0.1),可见判定 =
sinθ > 阈值/0.1 —— 0.35 ⇒ sinθ>3.5 永假 ⇒ 所有轴消失。

仿真量纲(MVP 复刻 + ScreenFactor 机制确认):⊥视线轴 clip 长度
恒 ≈0.97·screenFactor=gizmoSize;视线向轴 = gizmoSize·sinθ。
0.034 ⇒ sinθ>0.34 ⇒ θ<20° 隐藏(UE 正对轴隐藏语义),≥20° 可拖。
2026-10-03 02:02:37 +08:00
atdunbg 5cc9b14d73 fix: 旋转 snap 单位修正(度非弧度)+ 轴可见度阈值对齐 UE 正对轴隐藏语义
两个实锤问题(源码数学 + 数值仿真定位):
1. 旋转 snap 传了 glm::radians(15°)≈0.26,而库按"度"解释
  (docs/documentation.md:snap angles are in degrees,库内自行
   DEG2RAD)→ 实际步进 0.26° 形同虚设。改传 15.0
2. 正对视角两症状同根因(view-aligned axis degeneracy):
   - 轴⊥视线:悬停检测的射线-垂直平面求交 denom=0 → len=-1 →
     无法 hover(动视角即恢复)
   - 轴∥视线:屏幕投影无一阶位移 → 怎么拖都不动
   默认 mPlaneLimit=0.02 太小,"有高度差"视角下投影 sinθ≈0.17~0.34
   刚好高于阈值 = 看得见但几何上不可操作。提至 0.35:夹角 <20° 的
   轴自动隐藏(UE 同语义),>20° 正常可拖
2026-10-03 01:49:42 +08:00
atdunbg 83c1600947 feat: Gizmo local/world 空间切换 + Ctrl 吸附 + Inspector 轴色手输行
- GizmoState.localSpace(默认 local,模型轴旋转直观),T 键切换
  Manipulate 的 LOCAL/WORLD 模式;无父层级两模式 world 矩阵同源,
  decompose 写回链路不变
- Ctrl 按住吸附(UE Grid Snap 语义):平移 0.25m / 旋转 15° /
  缩放 0.1x;ImGuizmo 约定旋转 snap 单位为弧度
- 视口左上角 drawlist 叠加状态提示(Space/Snap,不占布局行)
- Inspector Transform 三行升级 UE 风轴色输入行(对齐 Gizmo 轴色):
  拖拽粗调 + Ctrl+Click 键入精确值
2026-10-03 00:56:27 +08:00
atdunbg e5161439dd refactor: TransformComponent 旋转存储改四元数(UE FTransform 语义)
欧拉角内部存储每帧矩阵↔欧拉往返,是 Gizmo 拖拽乱跳/缩放串轴的
结构性根源(74d2f6c 的单点公式修复治标不治本)。对齐 UE/Unity
方案:内部存 glm::quat,欧拉只在 UI 表皮。

- Components.h:m_Rotation 改 quat;Compose 用 mat4_cast;
  SetRotation(euler) 转存(q=qy·qx·qz);新增 SetRotationQuat/
  GetRotationQuat;GetRotation() 改派生欧拉(YXZ 序矩阵反解,
  仅 UI 显示/编辑用)
- ViewportPanel:Gizmo 写回 decompose 的 quat 直写,删除整个
  欧拉提取块
- PhysicsWorld:删 QuatToEulerYXZ/EulerYXZToQuat,改 ToJolt/
  ToGlm 分量直转;物理写回 SetRotationQuat;BodyRecord.lastRot
  改存 quat

验证(1 万随机姿态 round-trip):Gizmo 链路矩阵差 7.9e-07
(旧欧拉方案 3.56e-04,好 3 个数量级);万向锁 pitch=±90° 穿
表皮恒定;欧拉表皮误差 4.5e-04 为欧拉参数化固有病态(随
pitch→±90 指数增长,仅影响 UI 显示数值),热路径不经过。
2026-10-03 00:47:11 +08:00
atdunbg 74d2f6cdaa fix: Gizmo 旋转/缩放写回乱跳——eulerAngles 序约定与 Compose YXZ 失配
glm::eulerAngles 假定 XYZ 系约定,与 TransformComponent::Compose 的
YXZ 旋转序不一致:复合旋转下(如 Dynamic 实体被物理写入非零姿态)
decompose 出的角度按错误序重组 → 写回的 R ≠ 拖拽时的 R → 下一帧
Manipulate 拿到歪矩阵 → 旋转乱跳、缩放串轴。

改为按引擎 YXZ 序(R = Ry·Rx·Rz)从旋转矩阵直接提取欧拉角:
- 常规区:atan2 分子分母 cx 约掉,pitch 任意角精确(含 ±90° 邻域)
- float 真奇异(|pitch|=90°):组合角归 yaw、roll=0(UE 同策略),
  ±90° 两支 atan2 符号不同

验证(round-trip 1e4 样本,矩阵 Frobenius 差判据):
- 欧拉链 2.67e-04、全 SRT 链 3.56e-04(float32 极限)
- 万向锁邻域专项覆盖
2026-10-03 00:27:16 +08:00
atdunbg ce5e07879c fix: Gizmo 无法拖动 + Dynamic 提示行顶视口抖动
- Editor::Draw 每帧首调 ImGuizmo::BeginFrame():不调则
  mbOverGizmoHotspot hover 一次杆后永久置位(仅 BeginFrame 清零),
  GetMoveType 恒返 MT_NONE → 杆可见但永远抓不住
- Dynamic 刚体提示改 drawlist 叠加(Image 内左下角、纯英文):
  TextDisabled 占布局行 → 滚动条出现/消失循环 → avail 抖动 →
  渲染分辨率每帧重报(视口疯狂顶动);且默认字体无中文字形
2026-10-02 23:30:04 +08:00
atdunbg 3ec96acc0f feat: Editor Gizmo——ImGuizmo 平移/旋转/缩放操纵杆
- ImGuizmo 接入:Lib/ImGuizmo(src/ 布局)静态库,IMGUI_DEFINE_MATH_OPERATORS
- ViewportPanel:Vulkan→GL 投影转换(Y 翻回 + 深度 [-1,1])、
  SetDrawlist/SetRect 叠加绘制、decompose 写回 TransformComponent
- W/E/R 切换(UE 语义);拖拽中 BlockingMouse 门控(输入归 Gizmo)
- Dynamic 刚体选中提示:物理每帧写回,编辑被覆盖
- 修复:GizmoState.op 初值 0 无轴位(ImGuizmo::TRANSLATE=7)导致杆不绘制
- 点击拾取门控:抓杆时(IsOver/IsUsing)不发 pick 请求
2026-10-02 23:16:52 +08:00
atdunbg 56bb31638e 添加windows构建适配,禁用ImGui多视口功能 2026-10-02 21:53:26 +08:00
atdunbg b0d5ed4550 refactor: ShaderManager 引用计数句柄——shader 生命周期自动化
- ShaderObject 值类型 → ShaderRef(shared_ptr 语义,UE TShaderRef
  对标):拷贝共享所有权,最后一个引用析构时自动销毁 VkShaderEXT,
  持有方零清理代码——"新 pass 忘销毁"整类 bug 被机制消灭
- 删除公开 Destroy 与全部持有方手工 destroyShaderEXT(8 处);
  各类析构只清 sampler/layout/set 等其余原生句柄
- RecordFullscreen 参数收窄为 const ShaderRef&(裸 handle 无法
  再被误传);bind 点统一 GetHandle()
- Outline/SelectionMask 撤掉上一轮补的 Device&(不再需要)
- 验证:5s 探针自动退出 → exit 0、无 VUID-05137 泄漏、无 SIGSEGV
2026-10-02 20:20:07 +08:00
atdunbg 47dd879977 fix: 退出 SIGSEGV 与 VkShaderEXT 泄漏——生命周期三层秩序
- 根因:~Application 体内 Logger::Shutdown() 先于成员析构执行,
  Device 销毁期间 Vulkan validation 回调仍打日志 → spdlog 悬空调用
- 修复:app 置于 main 独立作用域,所有成员析构完毕后再 Shutdown
- 退出序 = waitIdle(~Application 体内)→ 成员析构(shader 等
  Device 子对象销毁)→ Logger::Shutdown
- 顺带注释掉窗口全透明(调试期遗留)
2026-10-02 20:09:12 +08:00
atdunbg 1449bea86d feat: HitProxy GPU 拾取 + UE 选中描边 + 编辑器拾取交互
- HitProxy 拾取:RG 管线渲染实体 id → host readback,点击帧
  PickEntity 精确命中;拾取按视觉几何(Model 联合包围球射线求交),
  与 Collider 解耦(UE/Unity 编辑器语义),命中打印实体名+碰撞体参数
- 选中描边(UE 语义):SelectionMask pass(R8Unorm 完整投影轮廓,
  无深度测试透墙可见)→ Outline pass(边缘检测 + alpha 叠加,
  必须注册在 Tonemap 后);edgeCut 贴视口边缘;widthPx=6 可调
- shader object 实证修复:VUID-08879 同程序 set layouts 超集一致、
  VUID-10065 push block 4×float 平铺、新 pass 析构销毁 VkShaderEXT
- RG:SideEffect() 保活"产出无 RG 内消费者"的 pass 链
- Editor:层次面板点击选中、Inspector 姿态编辑、Stats 面板拾取信息
- 相机 Update 改 const(姿态写入 Transform 引用,Camera 本身无状态变更)
2026-10-02 20:09:05 +08:00
atdunbg 1c4b6f37ff feat: Jolt 物理系统——刚体/碰撞体/物理调试图元 + 引擎时间单源
- PhysicsWorld:Jolt 集成,Dynamic/Kinematic/Static 刚体,姿态写回
  Transform(Unity FixedUpdate 语义,吃 scaled dt,暂停自动冻结)
- 碰撞体:Box(默认/手动 halfExtents)+ Mesh 碰撞体(Static→精确
  MeshShape / Dynamic·Kinematic→ConvexHull,数据源 = Mesh ctor 缓存
  的 CPU 位置副本,退化三角剔除,失败回退 Box 并告警)
- PhysicsDebugPass:物理调试图元每帧上传绘制(debug_line shader)
- Time:引擎时间单源(BeginFrame 落定 FrameTime,scaled/unscaled 分离)
- JoltPhysics 以 submodule 接入(官方 url)
2026-10-02 20:08:54 +08:00
atdunbg bfbb18f1a4 build: meshoptimizer 改为 git submodule(--depth=1)
Lib 依赖一律 submodule 不入库;历史中的在树源码记录已用 filter-branch 全量清洗
2026-09-30 09:22:30 +08:00
atdunbg 20f0035baa feat: ImGui docking Editor + 显示链/帧同步/输入语义重构
- App/editor: Hierarchy/Inspector/Viewport/Stats 四面板 + DockBuilder 默认布局,main.cpp 瘦身为场景搭建与生命周期
- 显示链: Tonemap→PresentTarget 经 ImGui::Image 呈现(零色偏),backbuffer 仅承载 ImGui;Direct/Viewport 双显示模式 + 面板分辨率上报
- 帧首同步 BeginFrameSync: resize/vsync/尺寸/模式意图统一帧首落定(修描述符悬垂);修复 vsync 意图未落 swapchain 导致的每帧重建交换链
- 输入: Editor 每帧判定键盘/鼠标归属(打字让位、Viewport hover/focus 放行、Fullscreen 全放行)
- 材质: roughness 独立槽位 + 非 glTF 金属/粗糙贴图映射;透明队列 depthWrite=FALSE + translucent 独立 shader + scratch 清帧
- FBX: 非蒙皮网格烘焙节点链全局变换(Z-up 旋转/单位换算落顶点)
- Hierarchy 行 ImGui ID 以 UUID 作用域(重名不冲突、改名不丢选中)
2026-09-30 00:24:53 +08:00
atdunbg b25fa29fe5 feat: 透明材质 alpha blending——独立队列 + 线性 over 混合 + 距离排序
透明来源:glTF alphaMode=BLEND(解析期读取,OBJ/FBX 恒不透明——
资产格式定义写死);MASK 后续演进。Material 加透明语义(入缓存键),
程序化测试资产 CreateTransparentPlane(RGBA 纯色纹理)。

SceneRenderer:透明材质实例收集进独立 scratch(仅主视图,不投
阴影——alpha 阴影后续演进),batch 按平均距离降序排序(远→近,
batchMats 同步重排),SSBO 扁平化顺序 = 排序后顺序。

ForwardPass 两段绘制:不透明(无混合 + 深度写)→ 透明(Shader
Object 动态状态切 SRC_ALPHA/ONE_MINUS_SRC_ALPHA 线性 over + 深度
只测不写);velocity 附件透明段直写(透明表面 velocity 即自身运动)。
frag alpha = albedo 贴图 a 直通,预乘在 tonemap 段(hard constraint
不变)。已知边界:TAA alpha 时域混合(透明边缘几帧渐显);同 batch
多实例不做实例级排序。
2026-09-25 16:57:27 +08:00
atdunbg 8dbfb9f554 feat: LOD 运行时开关(ImGui Features 横排,关 = 实例收集恒选 LOD0) 2026-09-25 16:39:16 +08:00
atdunbg a975845214 perf: BuildFrame 收集段优化——实体级粗剔除 + 数组化 batch 查找 + 合并矩阵写
定位数据(100 背包 + SunTemple,RTX 5060):GPU 全帧仅 ~3ms
(Forward 1.91 + Shadow 0.82),CPU BuildFrame 收集段 5.94ms 才是
帧率瓶颈。三段细分测量:pre 0.2 / each+cull 5.0 / flatten 0.4。

优化(collect 5.94 → 4.6ms,-23%):
1. Mesh 全局连续槽位(GetSlot);ViewScratch batch 查找 hash →
   数组寻址(slot × kMeshLodCount 槽)
2. Model 级联合包围球(各 mesh bounds 并集):实体在四视图全部
   不可见 → 跳过整个 mesh 循环(多 mesh 模型的粗剔除)
3. worlds/prevWorlds 合并为 stride-2 InstanceMatrices 单次写
   (每实例每视图 2 次随机堆访问 → 1 次,cache miss 减半)

遥测:Renderer::GetCollectMs 暴露收集段耗时。剩余 4.6ms 的进一步
压缩(并行收集 / GPU-driven culling)单独立项。
2026-09-25 08:08:51 +08:00
atdunbg d9b1742327 feat: LOD 网格分级——meshopt 解析期简化 + 按距离/级联选级
vendored meshoptimizer 1.2(submodule 环境故障改直接 vendor)。Mesh
多 LOD:各级索引连续拼进同一 index buffer(共享顶点,LodRange 区间
选择),解析期 meshopt_simplify 生成链(LOD0=原始,每级目标减半,
相对误差 1e-2,小网格复用上级);都在 worker 线程,加载零额外卡顿。

SceneRenderer 按 (mesh, LOD) 聚合 batch(lookup key = 指针<<8|lod):
主视图按屏占比近似 radius/dist 选级(阈值 10/20 倍半径);阴影级联
按级联序降级(越远越简)。Draw 按 batch.lod 选索引区间。

A/B 对比(100 背包 10×10,RTX 5060 Laptop,vsync off):
- 纯背包:LOD 开 93-95 fps / 关 90-107 fps —— 无显著差异
- 加 SunTemple:LOD 开 52-56 / 关 51-55 —— 无显著差异
结论:当前场景瓶颈不在顶点数(5060 顶点吞吐富余),LOD 基础设施
保留(几何复杂资产/低端 GPU/更大场景生效),帧率问题转向 Tracy
GPU 时间线定位真实瓶颈。

坑:LodRange 记录"LOD 不足复用最后一级"时 first 不得继续累加
(buffer 无重复数据,累加必越界——VUID-08798 抓的)。
2026-09-24 20:53:19 +08:00
atdunbg 34b593f734 fix: TRACY_ON_DEMAND——GPU context 宣告随连接重发
GpuNewContext 消息只在 TRACY_ON_DEMAND 下 DeferItem(连接后重发)。
程序启动即建 Tracy GPU context 而 GUI 晚连接,宣告丢失 → GUI 不认识
GPU context → pass zone 全部不显示(只见 CPU lane)。启用按需剖析:
连接建立后的帧才活跃,语义即 tracy 官方推荐用法。
2026-09-24 20:16:30 +08:00
atdunbg 3bfe28b027 feat: Tracy GPU 时间线——RG pass 级 GPU zone
RenderGraph 新增 IGpuProfiler 可选注入(pass 边界 Begin/End 钩子 +
帧尾 Collect,zone 覆盖 barrier+lambda+postBarriers),RG 本体不感知
任何 profiler 实现。Renderer 侧 TracyGpuProfiler 实现:TracyVkContext
(校准用临时 cb,池必须带 RESET_COMMAND_BUFFER_BIT——tracy 校准流程
对同一 cb 连续 begin 三次)+ TracyVkZoneTransient(pass 名是运行时
字符串)。TRACY_ENABLE 关闭全链路退化为空。

用法:Tracy GUI(0.14.1 配对)连接后按 pass 名看 GPU 段时间线,
与 ImGui 数表互补;LOD 调优的测量前置。
2026-09-24 19:45:17 +08:00
atdunbg 99e6712cdf feat: 模型异步加载——assimp 解析移后台线程(SunTemple 3s 卡顿消除)
Model 拆两段式:ParseFromDisk(static 纯 CPU,assimp 解析 → 几何 +
材质引用,worker 可调)/ FromParsed(GPU 构建:纹理上传 / mesh 缓冲 /
材质描述符,主线程),同步路径等价两段串行。第二个异步任务类型落地,
线程收割骨架抽成 Core/AsyncTask<T>(thread + atomic + Take,覆盖式
Run;pending 接力语义留调用方)。

实测(SunTemple 1056 mesh):解析 2.5s 全程在 worker,主线程持续
渲染;GPU 构建 0.56s。assets 层经 IAssetUploader/AssetManager 重载
传预解析产物(路径缓存去重,路径已缓存则产物丢弃)。

已知边界:纹理解码(backpack 4k PNG ~1s)仍在 GPU 段主线程——
Texture 需支持预解码像素注入,后续演进。
2026-09-24 19:31:14 +08:00
atdunbg 6df3be2846 feat: 透明窗口 + 实例预算扩容(修复斜光阴影静默截断)
透明窗口:SceneSettings 加 ClearColor/DrawSkybox,随 FrameSceneData
每帧下发到 SkyboxPass(替换硬编码清屏色);GLFW 透明帧缓冲已开,
tonemap 预乘 alpha 直通已有,链路全通。TAA alpha 时域混合为已知边界。

实例预算:kMaxInstances 16384 → 65536(四视图共享预算,100 实体
斜光时需求 16.4k 超限,cascade 2 实例被静默丢弃 = 用户看到的
"拖灯光方向部分背包阴影消失")。截断状态每帧落遥测 + ImGui 红字
(原 warn-once 对动态穿越阈值的场景等于没有告警)。
2026-09-24 18:54:01 +08:00
atdunbg fa9681aa41 fix: GPUDriven batch 实例连续性——多实体共享 mesh 时实例数组错乱
实例数组按实体序收集([实体A的mesh][实体B的mesh]),而 GPU 端
batch 语义要求同一 mesh 的实例连续(firstInstance 基址 +
gl_InstanceIndex 直查 SSBO)。多实体共享 Model 后 mesh i 的各实例
分散在数组各处,batch.first+i 落到别的 mesh 的变换上——第二颗
背包的几何全画到第一颗的相邻 mesh 位置(碎裂/缺失/近看缺块)。

修:剔除期实例先归入各自 batch(ViewScratch 持有 per-batch 实例
列表),拼接时按 batch 扁平化为连续区间,batch.first = 扁平化基址。
单实体路径行为不变(每 mesh 单实例天然连续),容量跨帧复用。
2026-09-24 16:23:47 +08:00
atdunbg 25a365fce7 feat: 环境异步加载——HDR 解码移后台线程,运行时切换不卡主线程
decode 是切换耗时大头(4k ≈ 130-160ms),搬出主线程后运行时切换
只剩 GPU 段(~40ms)。IblGenerator 拆两段式:DecodeHdr(static,
纯 CPU 无线程状态,worker 可调)+ FromDecoded(GPU 生成,主线程);
FromHdrEquirect 保留为同步便捷路径。Assets 层经 IAssetUploader
重载 + AssetManager 重载传递预解码产物(路径已缓存则丢弃去重)。
App 层单 in-flight 状态机:worker 只产出纯 CPU 像素数据,忙肋期
新请求记 pending 接力(只保留最终意图),OnUpdate 每帧收割 +
SetEnvironment 换绑,析构 join。

实测(RTX 5060 Laptop,4 实例网格 + 运行中连切 3 环境):
~180fps 无感知尖峰;0 validation。
2026-09-24 14:32:39 +08:00
atdunbg 641f79d89e fix: GPUDriven batch lookup 索引语义混淆——多实体共享 mesh 越界崩溃
ViewScratch.lookup 误存 worlds 索引却被当 batches 索引用。单实体时
每个 mesh 恰好只出现一次、两序号恒等,潜伏不炸;多实体共享同一
Model(2x2 实例网格实测)后 worlds 与 batches 分叉,first-insert
用 worlds.size() 索引 batches → vector 断言越界。
修:lookup 存 batches 索引,batch.first(GPU firstInstance 基址)
保持 worlds 索引,两语义各归其位。
2026-09-24 14:32:39 +08:00
atdunbg 08439ed015 fix: ImGuiLayer 析构提前到 Logger Shutdown 之前 + UI 计时段收窄
m_ImGuiLayer 在 Application 析构体显式 reset(否则成员析构序在
Logger::Shutdown 之后);UI 计时范围从 Run 循环收窄到实际 ImGui
构建段(ImGui 未启用时不再计零段)。
2026-09-24 13:44:15 +08:00
atdunbg b65856b8a5 feat: ImGui 调试面板分组整理
Scene/Rendering/Environment/GPU Timings/Frame 五个 CollapsingHeader;
功能开关用 checkbox 条件显隐联动参数(Bloom 勾选才出现 threshold/
strength),无参数开关横排平铺。
2026-09-24 13:44:15 +08:00
atdunbg 1d690067bd feat: IBL 预积分 GPU compute 化 + 环境资产入 Assets 层(动态切换)
CPU 逐像素卷积(venice 4k 阻塞 7.36s)→ 4 个 compute shader 一次性
提交:equirect→cube / irradiance / prefilter×5mip / BRDF LUT。
Cubemap 瘦身为纯资源持有类,生成职责移交 IblGenerator;
环境经 AssetManager 缓存(EnvRef),Renderer::SetEnvironment
动态换绑(RenderPass::OnEnvironmentChanged 广播)。
调试视图 +4:EnvCube / Irradiance / Prefilter / BRDF LUT。

性能前后对比(RTX 5060 Laptop,venice_dawn_1_4k.hdr):
- 改造前 CPU:7.36 s(主线程阻塞)
- 改造后 GPU:HDR decode 127-134ms + 资源 22-35ms + compute 17-19ms
  ≈ 165-190ms(约 40×);Neutral 兜底 ~6ms
- 运行中动态切换环境同量级(ms 级 GPU,decode 占大头)

冒烟:两构建目录 0 警告;运行期 0 validation(含两次环境切换 +
调试视图 9-12 轮播自动化验证)。
2026-09-24 13:23:59 +08:00
atdunbg 705250df76 feat: spdlog 显式初始化——控制台彩色 + 文件双 sink
Application 构造时 Logger::Initialize(APP logger:stderr 彩色 +
HelloVulkan.log 双写,warn 以上立即 flush),析构 Shutdown。
2026-09-24 13:23:52 +08:00
atdunbg a1c6ad20be feat: 特性开关 + Pass 调试视图接口(ImGui 可控,统一帧契约)
三个运行时开关(Shadows/TAA/Bloom)与 9 种调试视图(Final/
TemporalDiff/HDR/TAAColor/Bloom/Velocity/ShadowMap0-2)。

统一设计:开关状态收敛 FrameSceneData 契约,SceneRenderer 唯一
写入、各 pass 只读(零虚接口路由);调试视图经 tonemap push
int 路由已绑定的中间目标(声明超集、运行时零重绑);HDR 域
资源过同一 tonemap 保证观感可比。

- Shadows:ShadowPass lambda 空转 + frag cascadeSplits.w 短路
- TAA:alpha=0 直通 + SceneRenderer 停投影抖动(同一持有者同步)
- Bloom:3 子 pass 空转 + tonemap 强度归零
- Renderer 门面 Get/Set 转发,App ImGui 3 Checkbox + Combo
- PostProcessParams 瘦身为纯数值参数,showTemporalDiff 被取缔
2026-09-24 13:03:00 +08:00
atdunbg 708d714459 fix: 阴影采样器 CompareOp 反转(诊断残留 eGreater)+ 移除截图调试设施
根因:shadow sampler 停留在诊断期的 eGreater——比较语义全反,
全场 shade 被压到 0.3 变暗,唯独真影区 lit=1 反而全亮,
即"影子比环境亮、像光源"的现象。修回 eLess 后影子正常。

顺带清理:第 60 帧截图管线(HDRColor/ShadowMap0 → BMP)、
CPU 投影诊断日志、GetCaptureTarget 及 TRANSFER_SRC extraUsage。
2026-09-24 12:42:24 +08:00
atdunbg d3c3d0cdd9 fix: 影子中镜面高光残留导致"影子比天空亮"
掠射角下 GGX spec 分母 (4·NdotV·NdotL) 变小,HDR 值可达 8+;
阴影只把它乘 0.3,30% 的刺眼白仍比天空亮——观感即"影子发白"。

- spec 项按 shade 重映射(0.3→0,1.0→1):光源被遮挡,其镜面像
  应完全消失;diffuse 保留 0.3 作间接近似
- ShadowPass 改正面剔除渲阴影图(标准 acne 治法):封闭网格只写
  背面深度,正面深度天然比 ref 大一个表面厚度,从根上消自阴影
  acne;代价是单面薄片在光背面时漏写(plane 单面朝上不受影响)
2026-09-24 12:10:15 +08:00
atdunbg 42fd9d5f30 fix: 阴影光源错误绑定 FillLight + 截图 usage 警告 + nodiscard
- LightComponent 增加 castShadow 标志;灯收集排序键改为
  castShadow 方向光 > 方向光 > 点光(此前稳定排序把收集序靠前的
  FillLight 排到 lights[0],KeyLight 方向滑条自然不影响阴影)
- TextureDesc 增加 extraUsage:HDRColor 声明 TRANSFER_SRC,
  消除截图 copy 的 3 条 usage 不匹配运行时警告
- waitForFences nodiscard 返回值显式丢弃,消除编译警告
2026-09-24 11:59:18 +08:00
atdunbg 4d3963e291 fix: 阴影不可见——灯序依赖遍历顺序 + 光比失衡,新增帧截图调试能力
- 灯序修复(根因):entt Each 遍历序不保证创建序,lights[0] 曾落到
  点光(params.x=1)→ shader 阴影分支整个跳过。收集后 stable_sort
  方向光排前,确立"lights[0] = CSM 阴影光源"契约
- 光比失衡:IBL 环境与点光淹没主光阴影。KeyLight 5.0 + exposure
  默认 0.6 + ambientColor 0.07;阴影区环境光同步衰减(简化遮蔽)
- ShadowPass depth bias 0.006(acne 均值形态 vs 推过头丢阴影的折中)
- Renderer 帧截图能力:第 60 帧自动截 HDRColor/ShadowMap 落 BMP
  (ACES+gamma 还原屏幕观感),视觉调试自闭环不再依赖肉眼
- KeyLight 增加 intensity slider(0-20 运行时可调)
2026-09-24 01:49:11 +08:00
atdunbg 8c0a11d2e8 feat: CSM 软阴影——sampler2DShadow 硬件 PCF + 3×3 纹素 tap
- 阴影采样器开启硬件深度比较(compareOp=eLess + linear filter):
  单次采样 = 2×2 深度比较插值,clamp-to-border 白色语义不变
- ShadowFactor 改 3×3 纹素偏移 tap(等效 36 次比较),影中保留
  0.3 环境感并按 PCF 结果插值——阴影边缘软过渡,消除二值硬边锯齿
2026-09-23 23:18:36 +08:00
atdunbg 960bdf2662 refactor: 移除贴图文件名探测——资产读取保持确定性
按用户决定:OBJ/FBX 按材质定义(MTL/assimp 槽位)写死读取,
不做文件名猜测补槽;贴图缺失走默认纹理 + INFO 日志可见。
补槽职责归将来手动材质编辑接口。
2026-09-23 19:51:03 +08:00
atdunbg 2b875c9284 feat: OBJ 全量贴图接入——Material 加 AO 槽 + OBJ 工作流适配
- Material 槽位 4→5(+AO),参数 UBO binding 4→5;shader ambient
  diffuse 项乘 ao(IBL 遮蔽),AO 默认纹理白色
- OBJ 槽落点补全:map_Bump→HEIGHT(5)、map_Ns→SHININESS(7)、
  map_Ka→AMBIENT(3)(assimp OBJ importer 约定)——backpack 的
  法线贴图此前也从未加载
- MTL 导出遗漏(roughness.jpg/ao.jpg 在目录未被引用):约定文件名
  探测补槽(roughness/ao/occlusion × jpg/png),命中留 INFO 锚点
- 因子按工作流分支:glTF 用声明因子(MR 合图 B=金属度);
  OBJ metallic=0(specular 工作流无金属概念),roughness 有图→1,
  无图→Blinn-Phong Ns 换算 R=sqrt(2/(Ns+2))(补偿默认贴图 G=0.5)
2026-09-23 19:49:36 +08:00
atdunbg 9cc36af3f0 fix: 金属渲染塑料感——glTF MR 贴图未加载 + UV 翻转错误
- assimp 5.x glTF2 importer 把 MR 合图落在 METALNESS(15)/
  DIFFUSE_ROUGHNESS(16),不是 UNKNOWN(18)——此前 metallic 恒 0
  全走电介质路径。槽位查找改为优先级数组(先到先得)
- glTF 的 UV 本就是左上原点,aiProcess_FlipUVs 会把全部贴图上下
  颠倒(法线/MR 分布全反)——按扩展名区分 flip
- 材质加载 INFO 日志(贴图槽缺失会静默退化默认纹理,留诊断锚点)
2026-09-23 19:35:06 +08:00
atdunbg d2b878c077 refactor: Cubemap 拆掉 17 参超级构造——资源三元组 EnvTexture
- image/view/显存分配收口为 EnvTexture 单元(RHI 惯例:三者同生共死),
  Cubemap = 4 个 EnvTexture + 2 采样器的组合,构造 17 参 → 8 参
- UploadCube/UploadTexture2D 直接返回完整资源单元(view 创建收进
  helper),工厂函数只做"生成数据 → 上传 → 组装"
- 析构循环成对释放,杜绝半套泄漏;删除无调用者的 Get()/GetFaceSize()/
  GetPrefilteredMipCount()(shader mip 上限以常量契约)
2026-09-23 19:25:32 +08:00
atdunbg bd4ac6aa12 fix: IBL 全黑根因——RadicalInverseVdC 误用 bit_cast,Hammersley 序列错乱
- bit_cast<f32>(bits) 把反转后的整数当 IEEE754 位型解读(LearnOpenGL
  原文 float(bits) 是整数值转换)→ xi.y 越出 [0,1] → sqrt(负数) →
  BRDF LUT NaN + pre-filter 数据全垃圾 → ambient NaN 整屏黑
- 分离出的积分程序实测:texel(0,2) 样本 10 xi.y=2 → NaN;修后无 NaN
- 加 CPU 侧 fp16 NaN 自检(上传前最后一道防线),数据异常拒绝加载
  走中性兜底(不再解引用空指针)
- 中性兜底 1×1 图只建 1 mip(修 mipLevels=5 validation 警告)
- prefilter mip 拷贝偏移改累积制(此前用当前 dim×序号错位)
- prefilter/irradiance 绑定换带 mip 的环境采样器(shadowSampler
  maxLod=0 把 textureLod clamp 到最锐 mip)
2026-09-23 19:18:06 +08:00
atdunbg 6e107ac2ba fix: IBL 模型全黑——prefilter mip 拷贝偏移错位 + 采样器 maxLod 错误
- UploadCube 的 mip buffer 偏移误用当前 mip 尺寸×序号(mip 面递减,
  正确应为累积偏移)→ 各 mip 拷到 staging 错位数据,含未初始化 fp16
  NaN → 污染 ambient 输出整屏黑
- prefilter/irradiance 绑定改用环境采样器(maxLod=mipCount-1):
  shadowSampler maxLod=0 会把 textureLod 显式 lod clamp 到 0,
  pre-filter 永远停在最锐 mip
2026-09-23 19:07:53 +08:00
atdunbg 9269afaa18 feat: IBL 阶段③——pre-filtered cubemap + BRDF LUT,split-sum 镜面项接入
- pre-filter:GGX 重要性采样(Hammersley + van der Corput 位反转),
  128²×5 mip 分级 roughness,采样数 32<<mip 上限 256;UploadCube 泛化
  支持 mip 链([mip: 6面] 紧凑排布逐 mip 逐面拷贝)
- BRDF LUT:128² R16G16Sfloat 积分表((NdotV,roughness)→F 缩放/偏置),
  纯数学与环境无关,中性兜底同样生成;专用 clamp-to-edge 采样器
  (共享 border 白采样器在 uv=1.0 混入白色导致高 roughness 偏大)
- shader:specular = prefiltered(R, roughness×4) × (F·A+B),
  ambient = (diffuse + specular) × ambientColor
- Hammersley 分母修正为当次积分采样数(低 mip 偏差 bug)
- Debug 构建预积分 26s→14s(采样预算收敛 + pow 改乘法链;
  env 1024² 重采样为大头,compute 迁移后归零)
2026-09-23 19:04:25 +08:00
atdunbg fa25cb3348 feat: IBL 阶段②——漫反射辐照度卷积,PBR ambient 接入环境光
- Cubemap 构造期同步 CPU 半球 Lambert 积分(64×32 网格 ≈2048 采样/像素)
  生成 32² irradiance cube(漫反射是超低频信号,分辨率无需高)
- shader:kD 用粗糙度感知 FresnelSchlickRoughness + (1-metallic),
  N 采样 irradianceMap(Set2 binding 3)替换常数 ambient;
  ambientColor 保留为强度/色调乘子
- CreateNeutral 兜底:无 HDR 资产时 1×1 常数 cube,描述符恒有效
- FaceDir 改为返回单位方向(equirect 查表要求,修掉未归一化的畸变)
2026-09-23 18:50:34 +08:00
atdunbg 4b5990e6c8 feat: 环境背景管线(IBL 阶段①)——HDR 等距柱状转 cube + Skybox pass
- Cubemap:stbi_loadf 加载 .hdr,CPU 双线性重采样 6 面(GL 规范方向表),
  f32→fp16 转换后整块上传 eCubeCompatible image(R16G16B16A16Sfloat)
- SkyboxPass:HDRColor 的 first writer,承担清屏职责;无环境贴图自动
  退化为纯清屏(Forward 不受影响)
- frag 逐像素 NDC 反投影取世界方向(invVP push,投影变换不可线性插值)
- Forward color 附件 Clear→Load:几何合成在天空之上
- SceneRenderer 产出 skyInvVP = inverse(proj × mat4(mat3(view)))
2026-09-23 18:37:01 +08:00
atdunbg 89bf4256e8 feat: 解析式 PBR(Cook-Torrance GGX,metallic-roughness 工作流)
- 修 void* 指针算术警告(Texture::InitFromDds)
- Material:slot2 语义切为 metallicRoughness(glTF 约定 G=粗糙度
  B=金属度)+ 新增材质因子参数 UBO(binding 4,per-slot ring,
  创建时写死,metallic/roughness 因子乘 MR 贴图)
- simple_shader.frag:Blinn-Phong → Cook-Torrance(GGX 分布 +
  Smith 几何 + Schlick Fresnel,k=(r+1)²/8 解析光源版);金属不参与
  漫反射;环境光暂为常数(IBL 后续)
- Model:读 aiTextureType_UNKNOWN(glTF MR 落点)+ AI_MATKEY_
  METALLIC/ROUGHNESS_FACTOR(旧资产缺键退化为电介质 0/0.5);
  材质缓存键含因子
- main.cpp:演示场景换回 backpack(旧资产走因子路径验证 PBR 全链路)
- 旧 specular 贴图不再使用(语义被 MR 取代)
2026-09-23 18:01:14 +08:00
atdunbg 2c4efc5165 feat: 多光源——点光源类型(距离衰减,位置来自 Transform)
- LightComponent 加 Type(Directional/Point)+ radius;点光位置取
  实体 Transform(固有组件首次实际受益)
- LightData shader 契约扩为 4×vec4(color/direction/position/params,
  每灯 64B,未用字段置零);UBO 688→816B
- 光照循环按类型分支:方向光无衰减;点光 L 指向光源 + 平方距离衰减,
  衰减半径外跳过;CSM 阴影只来自第 0 盏方向光
- main.cpp:SunTemple 场景加暖橙火坑光 + 冷蓝填充光(点光);
  KeyLight/FillLight 改 designated initializer
2026-09-23 17:46:25 +08:00
atdunbg d8c481ce48 fix: Tracy 主线程命名(未命名线程在 GUI 显示 Unknown) 2026-09-23 16:44:55 +08:00
atdunbg b79df3c60e fix: tracy 子模块钉到 v0.14.1,与 GUI 协议版本对齐
master 开发提交(81c5005)的协议号已偏离 0.14.1 release,
GUI 连接报 incompatible protocol。子模块记录精确 commit hash,
clone --recursive 的机器拿到的一直是锁定的版本
2026-09-23 16:39:06 +08:00
atdunbg b6982d426f feat: Tracy profiler 接入(CPU 帧时间线)
- vendored tracy 子模块;TRACY_ENABLE 全局生效,宏关闭时零开销
- 插桩:Application::Run FrameMark(帧边界)、Renderer::Render、
  SceneRenderer::BuildFrame(ZoneScopedN)+ GPUDriven Collect
  (ZoneNamedN——同作用域第二个 zone 不能用 ZoneScopedN,变量名
  固定会重声明)、Scene::UpdateTransforms
- 覆盖 CPU 侧关键路径:帧时间线 / 场景收集 / transform 重算 / 录制提交
- 用法:游戏运行时打开 Tracy GUI(release 下载)即自动连接(8086 端口)
2026-09-23 16:10:46 +08:00
atdunbg 7d137fa9b1 refactor: Transform 字段封装 + dirty 标记(对齐 UE USceneComponent 真实现)
- 修正上一版"全量暴力重算":UE 实为 dirty 标记 + 惰性计算,前提是
  字段私有(SetRelativeLocation 等 setter 标脏 + 子级传播)
- TransformComponent:SRT 字段私有化,Set/Get/World()/IsDirty 访问器;
  Set* 变更才标脏;AddRotationY 便捷接口
- Scene::UpdateTransforms 只重算脏实体——静止实体零成本;
  层级(父子)传播在脏标记基础上扩展
- 迁移 Camera/SceneRenderer/main.cpp 到封装 API
2026-09-23 15:35:20 +08:00
atdunbg 57ab8901a9 refactor: transform world 矩阵改由帧循环统一重算,删除手动 Compose
- Scene::UpdateTransforms():渲染前全量 Compose(UE transform-update
  阶段模式),Application::Run 在 FlushDestroyQueue 后调用
- 用户代码永远不需要手动 Compose——Set*/直接写字段均安全;
  Compose 降级为引擎内部调用
- 选择全量而非 dirty 标记:字段直接写(t.rotation.y = x)无法可靠
  追踪脏,无条件重算零脚枪;万级实体内成本可忽略,更大规模再引入
  dirty+封装
- main.cpp/Camera 所有手动 Compose 调用点删除
2026-09-23 15:26:58 +08:00
atdunbg 1bc44e261b refactor: Camera 位姿统一到 TransformComponent(单一事实源)
- Camera 删除内部 position/yaw/pitch 状态:Update 把鼠标旋转与 WASD
  移动直接写回实体 Transform(rotation.y=yaw / rotation.x=pitch,
  移动基向量由实体旋转推导,有变更才 Compose)
- GetView(transform) = 实体世界矩阵的逆——相机即实体,视图矩阵无
  第二来源;Scene::GetPrimaryCamera 可选带出实体 Transform
- 默认朝向约定变化:rotation.y=0 = 看向 -Z(与旧 yaw=-90 等价)
- 消除 CameraComponent 位置与 Transform 脱节的遗留债
2026-09-23 15:14:29 +08:00
atdunbg 019c17c7e7 feat: 实体出生即挂 TransformComponent(UUID + Transform 固有化)
- CreateEntity 自动挂 IdComponent(已有)+ TransformComponent(identity),
  对齐 UE/Unity 语义(GameObject↔Transform 不可分离 / AActor 必有
  RootComponent)——游戏实体的本质是"空间中存在的东西"
- 调用点 AddComponent<Transform> 改 GetComponent(重复 emplace 会断言)
- 遗留不一致记录在案:CameraComponent 位置存于 Camera 内部而非
  Transform,后续统一
2026-09-23 15:07:00 +08:00
atdunbg 0ed7b4c5b8 perf: CSM 每级联独立剔除 + 光向 caster pad
- 场景剔除从 [主|阴影] 两视图扩展为 [主|级联0|级联1|级联2] 四视图:
  每级联只画自己盒内的 batch,消除原先 3× 全量阴影 draw 的浪费
- 实例数组拼接为四段,batch.first 加段基址保持段内连续;
  ViewScratch 分桶收集复用容量,稳态零堆分配
- ComputeCascades 沿光方向扩展级联深度范围(pad=30)——切片盒外
  caster(高树/城墙)投影进级联的影子不会被剔除裁掉,per-cascade
  剔除正确性的前提;只扩 z 不缩 XY,分辨率不受影响
- 遥测 shadow 计数 = 各级联求和
- SunTemple 1056 mesh 全量加载,validation 零警告
2026-09-23 14:59:08 +08:00
atdunbg c54f60c5ea feat: 材质系统 v1——4 纹理槽(albedo/normal/specular/emissive)+ 法线贴图
- Material:单贴图 → 4 槽(binding=Slot 枚举),MakeCacheKey 组合键
  保持材质去重(同贴图组合共享描述符集)
- Model:读取 aiTextureType DIFFUSE/NORMALS/SPECULAR/EMISSIVE 四类路径;
  aiProcess_CalcTangentSpace 生成切线(Vertex +tangent 属性,location 3);
  每槽默认纹理兜底(白/平法线(128,128,255)/黑/黑,1x1)
- 纹理 sRGB 语义按槽位:颜色图 SRGB 变体,数据图(Normal/Specular)线性
  UNORM——Texture 路径加载加 srgb 参数;修复 InitFromPixels 图文 format
  不一致的 validation 警告
- UBO 尾部追加 cameraPos(Blinn-Phong V 向量)
- simple_shader.frag:TBN 法线贴图(无切线/平法线退化回几何法线)+
  Blinn-Phong 高光(specular 图强度系数)+ 自发光直加
- SunTemple 实测:148 张 DDS 全槽位加载(96 张线性变体)、1056 mesh、
  validation 零警告
2026-09-23 14:50:10 +08:00
atdunbg 8253c31ad5 fix: 视锥剔除平面归一化(透视因数误剔)+ 材质去重 + 描述符池可增长
- Frustum::FromMatrix 平面归一化:Gribb-Hartmann 提取的 left/right/
  bottom/top 法线模长含 1/tan(fov/2) 因子(>1),未归一化时包围球
  距离测试被放大,视锥边缘物体被误剔(SunTemple 全场景可见)——
  修正注释:必须传 [0,1] 重映射矩阵
- 材质去重:Model 新增 MaterialCache(按贴图路径共享 Material),
  Mesh 改持 Material* 非拥有指针。1056 mesh 渲染只需 48 个描述符集
  (对齐 UE/Unity 材质实例化思路,大场景第一道闸门)
- DescriptorManager 池改可增长分块(chunk=4096,OutOfPool/Fragmented
  自动续块):材质集数量不可预估(1G 资产可能上万),固定上限迟早爆
2026-09-23 14:32:35 +08:00
atdunbg a0c92af5db feat: DDS 纹理加载器(BC 压缩直传 + mip 链,参考 Horizon 实现)
- Assets::Dds 解析器:中性 DdsFormat 枚举(Assets 层禁 Vulkan 头铁律),
  支持 legacy fourcc(DXT1/3/5/ATI1/2)+ DX10 头(BC1..BC7/RGBA8/BGRA8)
  + 未压缩 32bpp + 2D mip 链;数据不解码,直接上传由硬件采样解码
- Texture:按扩展名分发(.dds → InitFromDds);渲染侧负责格式映射与
  sRGB 语义(颜色图走 SRGB 变体,BC4/5/6H 数据图保持线性);view/sampler
  覆盖完整 mip 链;逐 mip copyBufferToImage(紧凑排布)
- Model::GetOrLoadTexture:FBX/DCC 路径反斜杠归一化(否则 Linux 全挂)
- DescriptorManager 池 1000 → 4096(SunTemple 1056 材质集打爆过;
  材质去重属材质系统任务)
- main.cpp 临时挂 SunTemple 场景验证:48 张 DDS 全部加载(2048²、12 mips),
  1056 mesh 渲染正常,validation 零警告
2026-09-23 14:21:39 +08:00
atdunbg 0328d4aaeb feat: CSM 级联阴影(3 级联 × 1024²,实用分割 + 纹素对齐)
- SceneRenderer::ComputeCascades:相机视锥实用分割(λ=0.6)→ 反投影
  取切片角点 → 光空间 AABB 拟合 → 纹素对齐(消除相机移动时阴影边缘
  闪烁)→ [0,1] 深度重映射
- Shadow pass 单 pass 渲 3 个 region,每级联 push 各自光空间 VP
- Forward:UBO lightSpace[3]+cascadeSplits(视图深度选级联),Set2
  三张级联图按 binding 0/1/2 采样;vertex 输出 fragViewDepth
- 光源视锥剔除用最远级联(覆盖全阴影范围);双视锥独立剔除铁律保留
- 旧单张 2048 全场景图退役;近处级联分辨率 ~128px/unit(约 2× 提升)
- 编译零错误、validation 零警告、8 秒冒烟通过(10 pass 全活跃)
2026-09-23 13:48:43 +08:00
atdunbg 27b7f8785c feat: TAA 时域抗锯齿(velocity + 邻域 clamp + 子像素抖动)
- Velocity buffer:Forward 第二颜色附件,vertex 计算 clip 空间运动向量
  (UBO 尾部追加 projNoJitter/prevView/prevProj;上一帧实例矩阵 SSBO
  Set0 binding2,与当前实例数组平行对齐——转动的物体速度正确不鬼影)
- 子像素抖动:SceneRenderer Halton(2,3)×8 序列,抖动只进光栅化投影
- TAA resolve pass:velocity 反推历史位置 + 3×3 邻域 clamp 去鬼影 +
  mix(cur,hist,0.9) 时域累积;TAAColor 自引用历史
- RG 历史读补 post-barrier:采样后恢复镜像 resting layout(TAA 的历史
  是 ColorAttachment layout,不恢复则下帧 entry oldLayout 脱节)
- 管线:Forward(HDR+Velocity) → TAA(TAAColor) → Bloom/Tonemap 改源 TAAColor
- 编译零错误、validation 零警告、8 秒冒烟通过(10 pass 全活跃)
2026-09-23 13:29:31 +08:00
atdunbg a29aed2803 feat: Pause Animation 开关(帧历史 diff 冻结验证用) 2026-09-23 13:11:29 +08:00
atdunbg c75832e73e feat: RenderGraph 帧历史资源(ReadHistory)+ TemporalDiff 验证 pass
- PassBuilder::ReadHistory:读上一帧槽位 ((slot-1+F) mod F) 的物理镜像,
  TAA/运动模糊等时域效果的前置能力,RG 最后一个已知边界补齐
- 历史读不建拓扑边(跨帧依赖由同队列提交序保证)、不入本帧状态链、
  不改生命周期;剔除照常参与(生产者存活判定)
- 历史 barrier:首帧走 first(old=Undefined),之后走 steady 且
  src=ALL_COMMANDS|MEMORY_WRITE 显式闭合跨帧可用性+可见性;
  有历史读的资源,常规 entry steady 的 src 同样升级(历史读打破了
  同槽位 fence 全等保前提)
- TemporalDiff 调试 pass:|上一帧−当前帧|×增益,静止=黑/运动边缘发光
  即链路正确;Tonemap binding2 经 push 开关切调试视图
- 编译零错误、validation 零警告、8 秒冒烟通过(9 pass 全活跃)
2026-09-23 13:05:10 +08:00
atdunbg 0de8498350 feat: 显存分配器迁移到 VMA(P1 路线第一项)
- vendored VulkanMemoryAllocator 子模块,唯一实现编译单元 VmaImpl.cpp
- MemoryAllocator 重写为 VMA 后端:接口从"Allocate(reqs)+手绑"升级为
  "创建即分配即绑定"(CreateBuffer/CreateImage/Destroy/Flush),
  VMA 要求资源与内存成对释放,约束成单一出口
- Buffer/Texture/RenderGraph 三处调用方迁移;staging 持久映射 + 按分配
  精确 flush(原来按整块 flush)
- 获得:TLSF 通用分配、DedicatedAllocation 自动启用、memory budget 感知、
  内存别名基础(TAA/复用前置)
- 编译零错误、validation 零警告、8 秒冒烟通过
2026-09-23 12:51:11 +08:00
atdunbg 6f10fd624d chore: gitignore 屏蔽本地 Docs 目录 2026-09-23 12:43:42 +08:00
atdunbg 718a658f5d refactor: 解耦重构——还清四处架构债(对齐 UE/Unity 分层)
- SceneRenderer:场景→渲染数据翻译层(UE FSceneRenderer / Unity CullingResults
  模式),收口相机/灯光/光空间矩阵/双视图剔除,Renderer 只编排
- pass 对象化(Unity ScriptableRenderPass 模式):8 个 pass lambda 拆为
  Passes::{Shadow,Forward,Bloom,Tonemap,ImGui,Present},BuildGraph 超级函数消失;
  SceneGpu/FullscreenGpu 承接共享装配
- 中性 AssetRef(Core 层):ECS 组件不再依赖 Assets 模块(Unity InstanceID 思路)
- IAssetUploader 接口:AssetManager 不感知 Vulkan 具体类,上传服务由渲染层
  VulkanAssetUploader 提供
- Renderer 公共 API 不变(App 层零改动),编译零错误 validation 零警告
2026-09-23 12:02:49 +08:00
atdunbg 91880d88fe fix: tonemap 输出预乘 alpha 匹配 PRE_MULTIPLIED 合成模式 + 调整地面位置 2026-09-23 11:35:05 +08:00
atdunbg fa9082706b feat: 双视图视锥剔除(相机/光源各自剔除)
- Frustum(Gribb-Hartmann):从 view-proj 行组合提取 6 平面,
  包围球相交测试;按 Vulkan [0,1] 深度约定取 near=row2
- Mesh 构造时缓存局部空间包围球(顶点包围盒 → 中心+半径)
- 实例收集重写:单趟遍历,每个 (实体, mesh 部件) 对相机视锥与
  光源视锥分别做剔除——相机外实体仍可投影子进视野,Shadow 必须用
  光源视锥独立剔除,不能用相机结果
- 实例矩阵数组 = [主视图段 | 阴影视图段],batch.first 全局下标;
  两视图共享同一份 ObjectBuffer SSBO
- 聚合改哈希(unordered_map),单趟填充替代原两趟遍历
- 面板显示 instances main/shadow 计数(剔除效果实时可见)

实测:1 背包 80 main / 80 shadow(修复前 shadow=21——near 平面
按 [-1,1] 矩阵误用 [0,1] 公式,光锥近半段被误剔)
2026-09-23 11:21:28 +08:00
atdunbg df4871a49b feat: 透明窗口支持 + 退出确认流程 + ImGui 总开关
用户自主改进批次:
- 透明窗口:GLFW_TRANSPARENT_FRAMEBUFFER + 窗口 opacity 0.5,
  swapchain composite alpha 按能力自适应(Opaque/Pre/PostMultiplied),
  Forward 清屏 alpha=0,tonemap 透传 HDR alpha——物体实体、背景透明
- 退出确认:Window 挂 WindowCloseCallback 发事件,App 侧 OnWindowClose
  置确认标记,OnRenderOverlay 弹 ImGui 确认框后 Close();
  主循环退出条件改为 m_Running(close 事件驱动)
- ImGui 总开关:` 键切换 ImGuiLayer::SetEnabled,禁用时跳过
  NewFrame/OnRenderOverlay/BuildDrawData 整段
- 帧率控制简化:去掉 LimitFps 开关,非 vsync 时恒按 target 限帧
  (默认 60,可调 ≥24);m_VSync 默认 false(Mailbox 不限速)
- 清理:Model.cpp const 修正,swapchain 日志带 present mode,
  GPU 遥测暂时注释

验证:0 error 0 warning,composite alpha 实测选 PreMultiplied(0x2),
present=Mailbox
2026-09-23 10:57:48 +08:00
atdunbg d8274e8bdc feat: 帧率控制——手动限帧 + VSync 开关
- Application 帧率控制 API:SetLimitFps/SetTargetFps/SetVSync。
  限帧目标最低 24fps(防止过低刷新导致交互卡死),无上限;
  vsync 开启时 present 自带节奏,手动限帧不生效
- 限帧策略:sleep 到剩 ~2ms 后自旋,精度高且不烧 CPU
- Swapchain 加 VSync 开关:ChoosePresentMode 按开关选
  Mailbox(低延迟垂直同步)/ Immediate(不限速可撕裂),
  FIFO 兜底;切换经 SetVSync → 重建 swapchain(复用 HandleResize)
- Scene Debug 面板新增 Frame 区块(vsync/limit/目标滑条)
- Run 循环新增 CPU 归属遥测(update/ui/render/total,每 120 帧)
2026-09-22 20:33:10 +08:00
atdunbg cef32ea1ba fix: Shadow pass 接入 GPUDriven batch,实例上限提至 16384
100 背包掉帧 + 警告刷屏的根因与修复:
1. backpack.obj 有 44 个 mesh 部件,100 实体 × 44 = 4400 实例
   打爆 4096 上限 → 每帧截断 + 告警刷屏。上限提至 16384
  (1MB/slot),告警改一次性
2. Shadow pass 仍逐实体逐部件画(4400 draw,CPU 提交瓶颈)。
   改为与 Forward 共用同一批 batch:Set0 绑 ObjectBuffer,
   lightVP 一次 push,44 draw 完成。shadow.vert 经
   gl_InstanceIndex 从 SSBO 取模型矩阵
3. 遥测补全:Application::Run 每 120 帧输出 CPU 归属
   (update/ui/render/total),Render 输出实例收集耗时——
   实测 100 背包 CPU 2.0ms/帧 GPU 0.6ms/帧 ≈ 500fps
2026-09-22 20:17:05 +08:00
atdunbg 6a3fcac072 fix: Shadow pass 接入 GPUDriven batch,实例上限提至 16384
100 背包掉帧 + 警告刷屏的根因:
1. backpack.obj 有 44 个 mesh 部件,100 实体 × 44 = 4400 实例
   打爆 4096 上限 → 每帧截断 + 告警刷屏。上限提至 16384
  (1MB/slot),告警改一次性
2. Shadow pass 仍逐实体逐部件画(4400 draw,CPU 提交瓶颈)。
   改为与 Forward 共用同一批 batch:Set0 绑 ObjectBuffer,
   lightVP 一次 push,44 draw 完成。shadow.vert 经
   gl_InstanceIndex 从 SSBO 取模型矩阵

实测(100 背包):GPU 总耗时 ~5.7ms(Shadow 2.9 + Forward 2.6 +
后处理 0.1),0 警告,帧率受 mailbox 垂直同步 pacing 限制
2026-09-22 20:06:43 +08:00
atdunbg 7fca721a24 feat: GPUDriven 实例化渲染——draw 数与实体数解耦
- UniformBuffer ring 支持 eStorageBuffer usage(GPUDriven 实例矩阵
  per-frame-slot,正确性保证与 UBO 一致:fence 等待后写入)
- DescriptorManager 加 UpdateStorageBuffer;池补 STORAGE_BUFFER 类型
- Set0 新增 binding1 = 实例矩阵 SSBO(vertex 读取,std430)
- Forward pass 重写:每帧从 ECS 收集实例矩阵(按 mesh 聚合 batch,
  两趟遍历保证 batch 区间连续),同 mesh 的 N 个实体合并为
  一次 drawIndexed(instanceCount=N),firstInstance 指向 batch 起点;
  vertex shader 经 gl_InstanceIndex 取模型矩阵,forward 不再用 push
- draw 数 = 唯一 mesh 数,与实体数量解耦(4096 实例上限,
  超出截断告警;聚合改哈希为后续 TODO)

验证:8 pass 全活,validation 0 error 0 warning,GPU Timings 面板
实测各 pass 毫秒,场景渲染与阴影正确
2026-09-22 19:46:17 +08:00
atdunbg 41e4cc85d5 feat: RenderGraph GPU timestamp profiling
- RG::SetQueryPool 可选注入;Execute 期每 pass 前后各写一枚
  writeTimestamp2(BOTTOM_OF_PIPE),索引按帧槽位分区 + 执行序排布
- 每帧录制开头 reset 本槽位查询区间(上一轮结果已在 fence 等待后读取)
- Renderer 在 WaitFence 后读 64bit 结果换算毫秒(timestampPeriod 缓存),
  经 GetPassNames/GetPassGpuMs 透出给 ImGui 面板显示
- 每槽位首帧跳过读取(查询尚未 primed,避免 VUID-09401)
2026-09-22 19:25:42 +08:00
atdunbg 908e688178 feat: ACES tonemap + Bloom 后处理链(Extract/BlurH/BlurV 半分辨率)
- ACES(Narkowicz) 经 push 开关启用,exposure 接 ImGui 滑条
- RG: TextureDesc 支持 sizeScale 相对尺寸;per-pass 渲染尺寸解析
  (半分辨率 pass 的 viewport 由此决定)
- RG Toposort 重写:RAW+WAR+WAW 按声明序逐资源建边,支持多写者
  ping-pong 资源(旧逻辑对 reader 连接所有 writer,ping-pong 成环)
- GetPhysicalView 缺物理镜像时 throw(ENGINE_FATAL 只记日志不中止)
- 全屏 pass 通用 16B push + 公共录制 lambda,Extract/Blur/Tonemap 复用
2026-09-22 19:18:51 +08:00
atdunbg 601fd8997c feat: 输入隔离——ImGui WantCapture 在 Window 层门控事件与轮询 2026-09-22 19:18:51 +08:00
atdunbg 952ba8135d feat: HDR 管线 + Tonemap pass,显示变换显式建模
管线:Shadow → Forward(写 HDRColor R16G16B16A16F) → Tonemap →
ImGui(loadOp=Load) → Present。对齐 UE/Unity 结构:场景线性值
进 HDR 离屏图,Tonemap(全屏三角,exposure push + gamma 编码)
显式完成显示变换,Backbuffer 只接收显示空间颜色。

- Swapchain 改 B8G8R8A8Unorm:Srgb 附件会硬件二次提亮,UI 泛白
  (#4296FA→#8CCBFC);Unorm 下写什么显示什么,编码责任归 Tonemap
- Shader object push range 规则落地:程序级声明——同一 pass 的
  全部 shader EXT range 定义必须一致(VUID-08878),layout 同样
  按 vertex|fragment 声明,push 调用点覆盖全部声明 stage(VUID-01796)
- Tonemap 装配:Set0 = HDR 图(per-frame-slot 描述符 + GetPhysicalView),
  clamp 占位待 ACES 替换,bloom/FXAA 等后处理在 HDR 域插 pass
- CMake 加 imgui_demo.cpp(ShowDemoWindow 链接)
- 修复:m_TonemapDescriptorSets 未 resize 即下标写入(启动即崩)

验证:5 pass 全活,validation 0 error 0 warning,UI 原生色彩无泛白
2026-09-22 19:00:56 +08:00
230 changed files with 76812 additions and 2251 deletions
+6
View File
@@ -1,4 +1,8 @@
.idea/
.trae/
# 运行日志(工作目录生成)
HelloVulkan.log
cmake-build-*/
out/
@@ -6,3 +10,5 @@ build/
# 目前只上传代码,暂不上传assets
assets/
# 本地文档暂不上传
Docs/
+18
View File
@@ -17,3 +17,21 @@
path = Lib/imgui
url = https://github.com/ocornut/imgui.git
branch = docking
[submodule "Lib/VulkanMemoryAllocator"]
path = Lib/VulkanMemoryAllocator
url = https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator.git
[submodule "Lib/tracy"]
path = Lib/tracy
url = https://github.com/wolfpld/tracy.git
[submodule "Lib/meshoptimizer"]
path = Lib/meshoptimizer
url = https://github.com/zeux/meshoptimizer.git
[submodule "Lib/JoltPhysics"]
path = Lib/JoltPhysics
url = https://github.com/jrouwe/JoltPhysics.git
[submodule "Lib/ImGuizmo"]
path = Lib/ImGuizmo
url = https://github.com/CedricGuillemet/ImGuizmo.git
[submodule "Lib/yaml-cpp"]
path = Lib/yaml-cpp
url = https://github.com/jbeder/yaml-cpp.git
+21 -2
View File
@@ -1,4 +1,22 @@
add_executable(HelloVulkan main.cpp)
# App 层源文件:main + editor/(docking 编辑器)+ scripts/(C++ 原生脚本)
file(GLOB EDITOR_SOURCES CONFIGURE_DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.h
${CMAKE_CURRENT_SOURCE_DIR}/editor/panels/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/editor/panels/*.h)
file(GLOB SCRIPT_SOURCES CONFIGURE_DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/scripts/*.cpp)
# editor 形态(HELLOVULKAN_EDITOR=ON)编入 editor/(docking 编辑器);
# game 形态只有 main + scripts(编辑器源码整体不进产物)
if(HELLOVULKAN_EDITOR)
add_executable(HelloVulkan main.cpp ${EDITOR_SOURCES} ${SCRIPT_SOURCES})
else()
add_executable(HelloVulkan main.cpp ${SCRIPT_SOURCES})
endif()
target_include_directories(HelloVulkan PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/editor)
target_link_libraries(HelloVulkan PRIVATE Engine)
@@ -33,7 +51,8 @@ file(GLOB_RECURSE SHADER_SOURCES
CONFIGURE_DEPENDS # 新增 shader 时自动重配
RELATIVE ${SHADER_SRC_DIR}
${SHADER_SRC_DIR}/*.vert
${SHADER_SRC_DIR}/*.frag)
${SHADER_SRC_DIR}/*.frag
${SHADER_SRC_DIR}/*.comp)
foreach (f ${SHADER_SOURCES})
auto_compile_shader(${f})
+505
View File
@@ -0,0 +1,505 @@
//
// AssetItemView.cpp:资产条目绘制单元实现。
//
#include "AssetItemView.h"
#include "AssetKind.h"
#include "EditorColors.h"
#include "ThumbnailCache.h"
#include "panels/AssetBrowserPanel.h" // kPrefabDragPayload / kModelDragPayload
#include "panels/ControllerEditorPanel.h" // kControllerDragPayload
#include "panels/MaterialEditorPanel.h" // kMaterialDragPayload / kTextureDragPayload
#include <Engine/Renderer/Model.h>
#include <Engine/Renderer/MaterialAsset.h>
#include <imgui.h>
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <ctime>
#include <set>
#include <string>
#include <unordered_map>
#include <utility>
namespace {
constexpr float kBoxRounding = 3.0f;
// 类别色压暗背板 + 描边(p0/p1 来自上一个 item rect)
void DrawBoxChrome(const ImVec2& p0, const ImVec2& p1,
const ImVec4& accent)
{
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddRectFilled(p0, p1,
ImGui::GetColorU32(ImVec4(accent.x * 0.25f,
accent.y * 0.25f,
accent.z * 0.25f, 1.0f)),
kBoxRounding);
dl->AddRect(p0, p1, ImGui::GetColorU32(accent), kBoxRounding);
}
// 名字按可用宽度截断(UTF-8 边界安全)
std::string TruncateToWidth(const std::string& s, float maxW)
{
if (ImGui::CalcTextSize(s.c_str()).x <= maxW) return s;
std::string out = s;
const char* ell = "..";
while (!out.empty() &&
ImGui::CalcTextSize((out + ell).c_str()).x > maxW)
{
out.pop_back();
while (!out.empty() && (static_cast<unsigned char>(out.back()) &
0xC0) == 0x80)
{
out.pop_back(); // 退到 UTF-8 起始字节
}
}
return out + ell;
}
// 已尝试按需加载的材质资产路径:加载失败不再每帧重试(资产缓存接管
// 成功路径,重复调用是缓存命中)
std::set<std::string>& MaterialLoadTried()
{
static std::set<std::string> s;
return s;
}
// 材质加载闸门:大目录(上百个材质)逐个同步加载(.material 解析 +
// GPU 纹理上传——Texture 构造内含 submit + fence 阻塞等待,单个材质
// 可含多张大贴图)会顶爆单帧 → "卡成 PPT"。闸门 = 每帧至多 1 个 +
// 按上次实测耗时冷却(1 帧≈16ms;重载 ≈ 冷却相应帧数),把加载占用
// 压到 ≤ ~50% 帧预算,剩余帧时间还给渲染
struct MaterialLoadGate
{
bool Allow()
{
const int frame = ImGui::GetFrameCount();
return frame != m_LastLoadFrame && frame >= m_CooldownUntil;
}
void Charge(double elapsedMs)
{
const int frame = ImGui::GetFrameCount();
m_LastLoadFrame = frame;
const int cool = static_cast<int>(elapsedMs / 16.0);
m_CooldownUntil = frame + 1 + std::min(cool, 30);
}
private:
int m_LastLoadFrame = -1;
int m_CooldownUntil = 0;
};
MaterialLoadGate& MaterialLoad()
{
static MaterialLoadGate s_Gate;
return s_Gate;
}
// 已烘焙材质缩略图对应的 (serial, editVersion)。资产重载(新 serial)
// 或参数编辑(editVersion++)后与当前不符 = 缩略图过期,须作废重烘
//(否则浏览器永远停在旧图)——记录时机 = 发起烘焙请求那一帧
std::unordered_map<std::string, std::pair<Engine::u64, Engine::u32>>&
BakedIdentity()
{
static std::unordered_map<std::string,
std::pair<Engine::u64, Engine::u32>> s;
return s;
}
// 材质球缩略图全部走编辑器缩略图缓存:引擎把预览球渲染一次后 GPU→CPU
// 回读,App 注入缓存(与贴图缩略图同一条出图路径),浏览器不再占用
// 引擎预览 tile 池——大目录滚动/材质编辑器预览互不饿死
void DrawCachedThumb(const ThumbView& v, float size, const ImVec2& p0)
{
float fw = size, fh = size;
if (v.w > 0 && v.h > 0)
{
if (v.w >= v.h) fh = size * static_cast<float>(v.h) / v.w;
else fw = size * static_cast<float>(v.w) / v.h;
}
const ImVec2 fp0(p0.x + (size - fw) * 0.5f, p0.y + (size - fh) * 0.5f);
ImGui::GetWindowDrawList()->AddImage(
v.tex, fp0, ImVec2(fp0.x + fw, fp0.y + fh));
}
// 缩略图/占位块:一个命中项 + 绘制层。
// interactive=true → InvisibleButton(带 ID,可承载拖拽源/点击;Dummy
// 无 ID,在其上 BeginDragDropSource 会断言);false → Dummy(行内小图
// 纯展示,不抢行 Selectable 的点击)。
// 出图时**不铺任何背板**——透明像素会被类别色染色;背板只在未出图时
// 作为占位(类别色压暗 + 短标签)。仅可见项才请求缩略图,离屏项不入
// 解码队列(大目录不再一次性排队,出图延迟可控)
void DrawThumbBox(const EditorContext& ctx,
const std::filesystem::path& path, float size,
EditorAsset::AssetKind kind, bool interactive)
{
const bool visible = ImGui::IsRectVisible(ImVec2(size, size));
if (interactive)
{
ImGui::InvisibleButton("##thumb", ImVec2(size, size));
}
else
{
ImGui::Dummy(ImVec2(size, size));
}
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
const ImVec4 accent = EditorColors::AssetColor(kind);
bool drawn = false;
if (visible && kind == EditorAsset::AssetKind::Material && ctx.assets)
{
// 材质资产缩略图 = 引擎烘焙的静态材质球(GPU→CPU 回读 → 缩略图
// 缓存;与贴图缩略图同一条出图路径)
auto& cache = EditorThumbnailCache::Get();
const std::string assetPath = path.generic_string();
// 当前材质身份:已加载才有;未加载时无法判新旧
Engine::Material* curMat = nullptr;
if (ctx.assets->IsMaterialLoaded(assetPath))
{
Engine::MaterialAsset* a =
ctx.assets->FindMaterialAsset(assetPath);
curMat = a ? a->GetMaterial() : nullptr;
}
const ThumbView v = cache.Peek(assetPath);
if (v.state == ThumbView::State::Ready)
{
// 失效检测:缓存缩略图对应的材质身份 vs 当前身份——资产
// 重载(新 serial,换贴图槽)/ 参数编辑(editVersion++)后
// 不符 = 过期,作废缓存让下一帧重烘(否则浏览器永远旧图)
bool stale = false;
if (curMat)
{
auto it = BakedIdentity().find(assetPath);
if (it != BakedIdentity().end() &&
(it->second.first != curMat->GetSerial() ||
it->second.second != curMat->GetEditVersion()))
{
stale = true;
}
}
if (!stale)
{
DrawCachedThumb(v, size, p0);
drawn = true;
}
else
{
cache.Invalidate(assetPath);
BakedIdentity().erase(assetPath);
}
}
else if (v.state != ThumbView::State::Pending && ctx.renderer)
{
if (curMat)
{
// 未烘焙/已失效:请求烘焙并登记当前身份(key 幂等
// keepalive,回读完成即进缓存)
ctx.renderer->RequestMaterialPreviewBake(
curMat, cache.BakeKeyForPath(assetPath));
BakedIdentity()[assetPath] = {
curMat->GetSerial(), curMat->GetEditVersion()};
}
else if (!MaterialLoadTried().count(assetPath) &&
MaterialLoad().Allow())
{
// 按需加载材质(闸门限流,避免大目录同帧卡顿)
MaterialLoadTried().insert(assetPath);
const auto t0 = std::chrono::steady_clock::now();
ctx.assets->LoadMaterialAsset(assetPath);
const double ms = std::chrono::duration<double,
std::milli>(std::chrono::steady_clock::now() - t0)
.count();
MaterialLoad().Charge(ms);
}
}
}
if (!drawn && visible && EditorAsset::IsThumbnailable(path))
{
const ThumbView v =
EditorThumbnailCache::Get().Request(path.generic_string());
if (v.state == ThumbView::State::Ready)
{
DrawCachedThumb(v, size, p0);
drawn = true;
}
}
if (!drawn)
{
// 占位:类别色压暗背板 + 描边;大框叠加短标签
DrawBoxChrome(p0, p1, accent);
if (size >= 28.0f)
{
const char* label = EditorAsset::KindShortLabel(kind);
const ImVec2 ts = ImGui::CalcTextSize(label);
ImGui::GetWindowDrawList()->AddText(
ImVec2(p0.x + (size - ts.x) * 0.5f,
p0.y + (size - ts.y) * 0.5f),
ImGui::GetColorU32(accent), label);
}
}
}
// 文件夹命中项(黄色)+ 短标签
void DrawFolderBox(float size, bool interactive)
{
if (interactive)
{
ImGui::InvisibleButton("##dir", ImVec2(size, size));
}
else
{
ImGui::Dummy(ImVec2(size, size));
}
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
DrawBoxChrome(p0, p1, EditorColors::kFolder);
if (size >= 28.0f)
{
const ImVec2 ts = ImGui::CalcTextSize("DIR");
ImGui::GetWindowDrawList()->AddText(
ImVec2(p0.x + (size - ts.x) * 0.5f,
p0.y + (size - ts.y) * 0.5f),
ImGui::GetColorU32(EditorColors::kFolder), "DIR");
}
}
} // namespace
namespace EditorAssetView {
void DrawDragSource(const std::filesystem::path& path,
const std::string& name)
{
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
const char* payload = nullptr;
switch (kind)
{
case EditorAsset::AssetKind::Material: payload = kMaterialDragPayload; break;
case EditorAsset::AssetKind::AnimClip: payload = kControllerDragPayload; break;
case EditorAsset::AssetKind::Prefab: payload = kPrefabDragPayload; break;
case EditorAsset::AssetKind::Model: payload = kModelDragPayload; break;
case EditorAsset::AssetKind::Texture: payload = kTextureDragPayload; break;
default: return;
}
if (ImGui::BeginDragDropSource())
{
const std::string assetPath = path.generic_string();
ImGui::SetDragDropPayload(payload, assetPath.c_str(),
assetPath.size() + 1);
ImGui::TextUnformatted(name.c_str());
ImGui::EndDragDropSource();
}
}
void DrawTooltip(const EditorContext& ctx,
const std::filesystem::path& path)
{
namespace fs = std::filesystem;
const std::string name = path.filename().string();
const std::string assetPath = path.generic_string();
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
ImGui::BeginTooltip();
ImGui::TextUnformatted(name.c_str());
ImGui::Separator();
ImGui::Text("type: %s", EditorAsset::KindLabel(kind));
std::error_code ec;
const auto sz = fs::file_size(path, ec);
if (!ec)
{
char szBuf[32];
if (sz >= 1024ull * 1024ull)
{
std::snprintf(szBuf, sizeof(szBuf), "%.1f MB",
static_cast<double>(sz) / (1024.0 * 1024.0));
}
else
{
std::snprintf(szBuf, sizeof(szBuf), "%.1f KB",
static_cast<double>(sz) / 1024.0);
}
ImGui::Text("size: %s", szBuf);
}
const auto ftime = fs::last_write_time(path, ec);
if (!ec)
{
// file_clock → system_clock 近似转换(C++17 无官方转换)
const auto sctp = std::chrono::time_point_cast<
std::chrono::system_clock::duration>(
ftime - fs::file_time_type::clock::now() +
std::chrono::system_clock::now());
const std::time_t tt = std::chrono::system_clock::to_time_t(sctp);
char tmBuf[32];
std::strftime(tmBuf, sizeof(tmBuf), "%Y-%m-%d %H:%M",
std::localtime(&tt));
ImGui::Text("modified: %s", tmBuf);
}
if (ctx.assets)
{
if (kind == EditorAsset::AssetKind::Model ||
kind == EditorAsset::AssetKind::Texture)
{
const bool loaded =
(kind == EditorAsset::AssetKind::Model)
? ctx.assets->IsModelLoaded(assetPath)
: ctx.assets->IsEnvironmentLoaded(assetPath);
ImGui::Text("state: %s", loaded ? "loaded" : "not loaded");
}
else if (kind == EditorAsset::AssetKind::Prefab)
{
ImGui::TextDisabled(
"template — drag into scene / double-click");
}
if (kind == EditorAsset::AssetKind::Model)
{
if (const Engine::Model* m =
ctx.assets->GetModelByPath(assetPath);
m != nullptr)
{
ImGui::Text("meshes: %u, bones: %u, clips: %u",
static_cast<unsigned>(m->GetMeshes().size()),
m->GetBoneCount(),
static_cast<unsigned>(m->GetClips().size()));
}
}
}
ImGui::TextDisabled("%s", assetPath.c_str());
ImGui::EndTooltip();
}
ItemResult DrawRow(const EditorContext& ctx,
const std::filesystem::path& path, bool selected)
{
ItemResult res;
const std::string name = path.filename().string();
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
ImGui::PushID(name.c_str());
// 内联小缩略图 / 类别色占位块(非交互:行点击归 Selectable)
DrawThumbBox(ctx, path, ImGui::GetTextLineHeight(), kind, false);
ImGui::SameLine();
// 名称(类别色提亮)+ 类别色 hover/选中高亮
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::AssetNameColor(kind));
ImGui::PushStyleColor(ImGuiCol_Header,
EditorColors::AssetHighlightColor(kind));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered,
EditorColors::AssetHighlightColor(kind));
ImGui::PushStyleColor(ImGuiCol_HeaderActive,
EditorColors::AssetHighlightColor(kind));
res.clicked = ImGui::Selectable(name.c_str(), selected,
ImGuiSelectableFlags_AllowDoubleClick);
ImGui::PopStyleColor(4);
if (res.clicked && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
{
res.doubleClicked = true;
}
DrawDragSource(path, name);
if (ImGui::IsItemHovered())
{
DrawTooltip(ctx, path);
}
ImGui::PopID();
return res;
}
ItemResult DrawTile(const EditorContext& ctx,
const std::filesystem::path& path,
float tileSize, bool selected)
{
ItemResult res;
const std::string name = path.filename().string();
const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path);
const ImVec4 accent = EditorColors::AssetColor(kind);
ImGui::PushID(name.c_str());
ImGui::BeginGroup();
DrawThumbBox(ctx, path, tileSize, kind, true);
// 选中/hover 描边(叠在缩略图上)
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
const bool hovered = ImGui::IsItemHovered();
if (selected || hovered)
{
ImGui::GetWindowDrawList()->AddRect(
p0, p1, ImGui::GetColorU32(accent), kBoxRounding, 0,
selected ? 2.0f : 1.0f);
}
res.clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
{
res.doubleClicked = true;
}
DrawDragSource(path, name);
if (ImGui::IsItemHovered())
{
DrawTooltip(ctx, path);
}
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::AssetNameColor(kind));
ImGui::TextUnformatted(TruncateToWidth(name, tileSize).c_str());
ImGui::PopStyleColor();
ImGui::EndGroup();
ImGui::PopID();
return res;
}
bool DrawFolderRow(const std::filesystem::path& dir)
{
const std::string dname = dir.filename().string();
ImGui::PushID(dname.c_str());
DrawFolderBox(ImGui::GetTextLineHeight(), false);
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::kFolder);
const bool clicked = ImGui::Selectable(dname.c_str());
ImGui::PopStyleColor();
ImGui::PopID();
return clicked;
}
bool DrawFolderTile(const std::filesystem::path& dir, float tileSize,
bool selected)
{
const std::string dname = dir.filename().string();
ImGui::PushID(dname.c_str());
ImGui::BeginGroup();
DrawFolderBox(tileSize, true);
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
if (selected || ImGui::IsItemHovered())
{
ImGui::GetWindowDrawList()->AddRect(
p0, p1, ImGui::GetColorU32(EditorColors::kFolder), kBoxRounding,
0, selected ? 2.0f : 1.0f);
}
const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left);
ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::kFolder);
ImGui::TextUnformatted(TruncateToWidth(dname, tileSize).c_str());
ImGui::PopStyleColor();
ImGui::EndGroup();
ImGui::PopID();
return clicked;
}
} // namespace EditorAssetView
+46
View File
@@ -0,0 +1,46 @@
//
// AssetItemView:资产条目的绘制单元(列表行 / 网格瓦片 / 目录 / 拖拽源 /
// tooltip)。Content Browser 与材质纹理选择器共用同一套视觉与交互。
//
#ifndef HELLOVULKAN_ASSETITEMVIEW_H
#define HELLOVULKAN_ASSETITEMVIEW_H
#include "EditorContext.h"
#include <filesystem>
#include <string>
namespace EditorAssetView {
// 单击 / 双击结果(调用方决定选中与打开语义)
struct ItemResult {
bool clicked = false;
bool doubleClicked = false;
};
// 拖拽源:按资产类别发出对应 payload(材质/控制器/预制体/模型/纹理)
void DrawDragSource(const std::filesystem::path& path,
const std::string& name);
// 统一 tooltip(类型/大小/修改时间/加载状态)
void DrawTooltip(const EditorContext& ctx,
const std::filesystem::path& path);
// 列表行:内联小缩略图 + 彩色短标签 + 类别色名称
ItemResult DrawRow(const EditorContext& ctx,
const std::filesystem::path& path, bool selected);
// 网格瓦片:缩略图/类别色占位块 + 名称(截断到瓦片宽)
ItemResult DrawTile(const EditorContext& ctx,
const std::filesystem::path& path,
float tileSize, bool selected);
// 目录(文件夹色):返回是否被点击(选中/进入)
bool DrawFolderRow(const std::filesystem::path& dir);
bool DrawFolderTile(const std::filesystem::path& dir, float tileSize,
bool selected);
} // namespace EditorAssetView
#endif //HELLOVULKAN_ASSETITEMVIEW_H
+122
View File
@@ -0,0 +1,122 @@
//
// AssetKind.h:资产分类的单一事实来源(Content Browser / 编辑器共用)。
//
// 原先分类逻辑散在 AssetBrowserPanel.cpp 匿名 ns(Kind/KindOf/KindTag),
// 现在上提为头文件:面板、拖拽源、缩略图缓存按同一份扩展名表分派。
//
#ifndef HELLOVULKAN_ASSETKIND_H
#define HELLOVULKAN_ASSETKIND_H
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <string>
namespace EditorAsset {
// 资产类别(按扩展名分派;同时是类型过滤的身份)
enum class AssetKind {
Model, Texture, Scene, Material, AnimClip, Prefab, Other
};
// 统一小写扩展名(含点,如 ".png";无扩展名 = 空串)
inline std::string LowerExt(const std::filesystem::path& p)
{
std::string ext = p.extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
return ext;
}
// 纹理扩展名表(单一事实来源:分类 / 缩略图 / 材质纹理槽选择器共用)。
// 口径 = 引擎 Texture 可直接加载者:
// stb_image 8-bit 系 png/jpg/tga/bmp/psd/gif/pnm
// + .hdr(环境贴图,stbi_loadf / stbi_load 走内置 hdr→ldr)
// + .dds(引擎 DdsLoader,BC 压缩直传)
inline bool IsTexturePath(const std::filesystem::path& p)
{
static const char* kExts[] = {
".png", ".jpg", ".jpeg", ".tga", ".bmp", ".psd",
".gif", ".ppm", ".pgm", ".pnm", ".hdr", ".dds"};
const std::string ext = LowerExt(p);
for (const char* e : kExts)
{
if (ext == e) return true;
}
return false;
}
// 扩展名 → 类别
inline AssetKind KindOfPath(const std::filesystem::path& p)
{
const std::string ext = LowerExt(p);
if (ext == ".gltf" || ext == ".glb" || ext == ".fbx" ||
ext == ".obj")
{
return AssetKind::Model;
}
if (IsTexturePath(p))
{
return AssetKind::Texture;
}
if (ext == ".scene")
{
return AssetKind::Scene;
}
if (ext == ".material")
{
return AssetKind::Material;
}
if (ext == ".animctrl")
{
return AssetKind::AnimClip;
}
if (ext == ".prefab")
{
return AssetKind::Prefab;
}
return AssetKind::Other;
}
// 全称标签(tooltip / 网格标题;替代原下标对齐数组)
inline const char* KindLabel(AssetKind kind)
{
switch (kind)
{
case AssetKind::Model: return "Model";
case AssetKind::Texture: return "Texture/Environment";
case AssetKind::Scene: return "Scene";
case AssetKind::Material: return "Material";
case AssetKind::AnimClip: return "Controller";
case AssetKind::Prefab: return "Prefab";
default: return "File";
}
}
// 列表类型列的短标签(替代原 "[M]"/"[T]" 前缀)
inline const char* KindShortLabel(AssetKind kind)
{
switch (kind)
{
case AssetKind::Model: return "MDL";
case AssetKind::Texture: return "TEX";
case AssetKind::Scene: return "SCN";
case AssetKind::Material: return "MAT";
case AssetKind::AnimClip: return "CTRL";
case AssetKind::Prefab: return "PFB";
default: return "FILE";
}
}
// 可解码为 8-bit 缩略图的扩展名:stbi_load 原生支持者(含 .hdr ——
// stbi_load 内部走 hdr→ldr 色调映射)。仅 .dds 例外(BC 压缩块,需
// 专用解码器)→ 占位块
inline bool IsThumbnailable(const std::filesystem::path& p)
{
return IsTexturePath(p) && LowerExt(p) != ".dds";
}
} // namespace EditorAsset
#endif //HELLOVULKAN_ASSETKIND_H
+619
View File
@@ -0,0 +1,619 @@
//
// Editor.cpp:docking 骨架实现。
//
// 默认布局 = UE5 出厂布局(首帧 DockBuilder 一次,之后 imgui.ini 接管):
// ┌──────────┬──────────────────────┬───────────┐
// │ Outliner │ │ │
// │ │ Viewport │ Details │
// │ │ (+Stats tab) │ │
// ├──────────┴──────────────────────┤ │
// │ Content Browser(左+中通栏) │ │
// └─────────────────────────────────┴───────────┘
//
#include "Editor.h"
#include "ThumbnailCache.h" // 缩略图缓存(每帧 BeginFrame)
#include "panels/AssetBrowserPanel.h"
#include "panels/HierarchyPanel.h"
#include "panels/InspectorPanel.h"
#include "panels/MaterialEditorPanel.h"
#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>
#include <imgui.h>
#include <imgui_internal.h> // DockBuilder*(内部 API)
#include <ImGuizmo.h>
#include <filesystem>
#include <fstream>
using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h)
Editor::Editor() = default;
Editor::~Editor() = default;
void Editor::Init(const EditorContext& ctx)
{
m_Ctx = ctx;
// PIE 运行/暂停镜像指针:指向本对象成员(ctx 拷贝后补接,面板只读;
// 权威 = GameFlow,镜像在 Draw 开头同步——面板接口保持 const bool*)
m_Ctx.playState = &m_PlayingMirror;
m_Ctx.pausedState = &m_PausedMirror;
// PIE 转换副作用挂监听(GameFlow 不知场景;快照/恢复是编辑器职责)
if (m_Ctx.flow)
{
m_Ctx.flow->AddListener(
[this](GameFlowState, GameFlowState to) {
if (to == GameFlowState::Loading) OnFlowEnterLoading();
else if (to == GameFlowState::Stopped) OnFlowEnterStopped();
// →Running/→Paused:timeScale 由 GameFlow 权威处理,
// 编辑器无额外副作用
});
}
m_Hierarchy = std::make_unique<HierarchyPanel>();
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();
m_ControllerEditor = std::make_unique<ControllerEditorPanel>();
m_Ctx.controllerEditor = m_ControllerEditor.get();
m_Ctx.selected = &m_Selected;
// 旧布局一次性迁移:面板改名(Hierarchy→Outliner / Inspector→Details /
// Asset Browser→Content Browser)= docking 身份全变,旧 imgui.ini 对
// 新窗口名失效(新窗口首启浮动,而默认布局又因 dock node 非空不再
// 重建)。检测"有旧名无新名" → 删 ini 让 UE 默认布局重建一次。
// 此刻 ImGui 首帧 NewFrame 还未读盘(NewFrame 内才 LoadIniSettings),
// 删文件干净无副作用;新式 ini 或无 ini 不动
if (ImGui::GetCurrentContext() && ImGui::GetIO().IniFilename)
{
const char* path = ImGui::GetIO().IniFilename;
std::ifstream file(path);
if (file)
{
const std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
file.close();
if (content.find("[Window][Hierarchy]") != std::string::npos &&
content.find("[Window][Outliner]") == std::string::npos)
{
std::error_code ec;
std::filesystem::remove(path, ec);
ENGINE_INFO("Editor: legacy imgui.ini removed, "
"UE5 default layout will be rebuilt");
}
}
}
}
// ---- PIE(Play-In-Editor)----
// →Loading 监听器:Play 半程。当前场景 → 内存 YAML 快照 → 切 gameView
//(Viewport 面板内嵌 primary 相机渲染,UE PIE 语义——编辑器 UI 保持
// 可用,不切全屏)→ 同步加载完成即 CompleteLoading(快照是同步操作,
// 不完成的 Loading 会卡死主循环的物理/脚本门控)。快照失败 → RequestStop
// 回滚(现场不动)
void Editor::OnFlowEnterLoading()
{
if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer || !m_Ctx.flow) return;
m_PlaySnapshot = Serialization::SceneSerializer::SerializeToString(
*m_Ctx.scene, *m_Ctx.assets, "PlaySnapshot");
if (m_PlaySnapshot.empty())
{
ENGINE_ERROR("PIE: snapshot failed, play aborted");
m_Ctx.flow->RequestStop(); // 回滚 → 触发 →Stopped 监听器(空快照跳过恢复)
return;
}
m_Gizmo.dragging = false; // 编辑态拖杆中点 Play:杆运行态不出,复位
m_Ctx.renderer->SetGameView(true);
m_Ctx.flow->CompleteLoading(); // 同步加载完成 → Running(解冻)
ENGINE_INFO("PIE: playing (scene snapshot {} bytes)",
m_PlaySnapshot.size());
}
// →Stopped 监听器:Stop 半程。快照原子换血恢复 → 回编辑器渲染。
// 换血后所有 Entity 句柄悬空:清选中(描边同步清)、Gizmo 拖拽态
// 复位。物理 body/约束/角色由对账系统随实体重建/清理,无需干预。
// 恢复失败(快照损坏,理论罕见)也必须回编辑态,现场留给用户
// File→Load 或退出前 Save。空快照 = 快照失败回滚(未真 Play 过),跳过恢复
void Editor::OnFlowEnterStopped()
{
if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer) return;
if (!m_PlaySnapshot.empty())
{
if (!Serialization::SceneSerializer::DeserializeFromString(
*m_Ctx.scene, *m_Ctx.assets, m_PlaySnapshot))
{
ENGINE_ERROR("PIE: snapshot restore failed (scene is now the "
"played-out state)");
}
m_PlaySnapshot.clear();
}
m_Ctx.renderer->SetGameView(false);
SelectEntity(entt::null);
m_Gizmo.dragging = false;
ENGINE_INFO("PIE: stopped, editor scene restored");
}
void Editor::TogglePlay()
{
if (!m_Ctx.flow) return;
IsPlaying() ? m_Ctx.flow->RequestStop() : m_Ctx.flow->RequestStart();
}
// PIE 暂停(UE Pause 语义):Running ↔ Paused。运行态才有意义
//(编辑器态本来就冻结,Pause 按钮不出现)
void Editor::TogglePause()
{
if (!m_Ctx.flow) return;
IsPaused() ? m_Ctx.flow->RequestResume() : m_Ctx.flow->RequestPause();
}
void Editor::Draw()
{
// 缩略图缓存推进(解码限量 / 上传就绪 / LRU 淘汰):ImGui::NewFrame
// 已由框架 ImGuiLayer 完成,此处处于面板绘制之前
EditorThumbnailCache::Get().BeginFrame();
// 材质球缩略图烘焙:排空引擎异步回读结果 → 注入缩略图缓存(浏览器
// 材质条目随后从缓存出图,不再占用引擎预览 tile 池)
if (m_Ctx.renderer)
{
Engine::u64 key = 0;
std::vector<unsigned char> pixels;
Engine::u32 w = 0, h = 0;
while (m_Ctx.renderer->PollMaterialPreviewBake(key, pixels, w, h))
{
EditorThumbnailCache::Get().CompleteBake(
key, pixels.data(), static_cast<int>(w), static_cast<int>(h));
}
}
// PIE 镜像同步(面板只读指针消费;权威 = GameFlow,此处适配)
if (m_Ctx.flow)
{
m_PlayingMirror = m_Ctx.flow->IsIn(GameFlowState::Running) ||
m_Ctx.flow->IsIn(GameFlowState::Paused);
m_PausedMirror = m_Ctx.flow->IsIn(GameFlowState::Paused);
}
// ImGuizmo 每帧首调(官方约定):内部会清 mbOverGizmoHotspot 等
// 帧级状态——不调则 hover 过一次杆后 hotspot 永久置位,GetMoveType
// 恒返 MT_NONE → 杆可见但永远抓不住(只能看不能拖)
ImGuizmo::BeginFrame();
// host 窗口:铺满主视口,承载菜单栏 + DockSpace
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->Pos);
ImGui::SetNextWindowSize(viewport->Size);
ImGui::SetNextWindowViewport(viewport->ID);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin("##EditorDockHost", nullptr,
ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus);
ImGui::PopStyleVar(3);
DrawMenuBar();
DrawToolbar();
DrawDockSpace();
ImGui::End();
// 常规面板窗口(窗口名 = docking 身份)
if (m_ShowHierarchy)
{
const entt::entity prev = m_Selected;
m_Hierarchy->Draw(m_Ctx, m_Selected);
// 树点选同步描边(面板直写 m_Selected,不走 Viewport 的
// SelectEntity 返回值通道;对比前后变化触发)
if (m_Selected != prev) {
SelectEntity(m_Selected);
}
}
if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected);
if (m_ShowViewport)
{
if (auto picked = m_Viewport->Draw(m_Ctx, m_VpInput, m_Gizmo,
m_Selected)) {
SelectEntity(*picked); // Viewport 点击拾取 → 选中联动 Inspector/Hierarchy
}
}
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 门控
DrawQuitModal();
DrawSceneModals();
}
// 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里
// 只决定"剩余输入给不给游戏侧"。UI 被隐藏(反引号)时不会走到
// Draw → m_VpInput 停在旧值,谓词调用方(SandboxApp)须先查
// GetImGuiEnabled() 再问这里。
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)
{
return false;
}
// 编辑器形态:Viewport 面板持焦才拥有键盘(UE Scene view 语义)
return !m_VpInput.focused;
}
bool Editor::BlockingMouse() const
{
// Gizmo 拖拽中:鼠标归操纵杆,不给游戏侧相机(松手自动恢复)
if (m_Gizmo.dragging) return true;
// 游戏形态:沿用 ImGui 全局判定(UI 隐藏时 WantCaptureMouse=false)
if (m_Ctx.renderer &&
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
{
return ImGui::GetIO().WantCaptureMouse;
}
// 编辑器形态:悬停 Viewport 才放行(模态框遮挡时 hovered=false)
return !m_VpInput.hovered;
}
void Editor::DrawMenuBar()
{
if (!ImGui::BeginMenuBar())
{
return;
}
if (ImGui::BeginMenu("File"))
{
// 运行态禁存取:Save 会把"玩出来的状态"当编辑结果落盘,
// Load 会撕掉快照恢复链(UE 运行态同样禁)
ImGui::BeginDisabled(IsPlaying());
if (ImGui::MenuItem("Save Scene...", nullptr))
{
m_ShowSaveModal = true;
}
if (ImGui::MenuItem("Load Scene...", nullptr))
{
m_ShowLoadModal = true;
}
ImGui::EndDisabled();
ImGui::Separator();
if (ImGui::MenuItem("Quit", "Alt+F4"))
{
RequestQuit();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View"))
{
ImGui::MenuItem("Outliner", nullptr, &m_ShowHierarchy);
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();
}
// 主工具栏(UE Main Toolbar 语义):菜单栏下方独立一行,Play 组水平
// 居中。编辑态 = [Play](绿);PIE 运行态 = [Pause][Stop](暂停高亮黄 /
// 停止橙红)。kDisplayDirect 全屏预览是发布形态预览,不在此处
void Editor::DrawToolbar()
{
const ImGuiIO& io = ImGui::GetIO();
// Ctrl+P 全局热键:编辑态进 PIE,运行态停回编辑器(双向)。
// 文本输入中(改名/路径框)不触发,防打字误切
if (io.KeyCtrl && !io.WantTextInput &&
ImGui::IsKeyPressed(ImGuiKey_P, false))
{
TogglePlay();
}
// 按钮加大触感(工具栏行高 = 一个按钮高;DockSpace avail 自适应)
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12.0f, 4.0f));
const ImGuiStyle& st = ImGui::GetStyle();
const char* pauseLabel = IsPaused() ? "Resume" : "Pause";
// Play 组宽度 → 水平居中(编辑态 1 钮 / 运行态 2 钮)
float groupW = 0.0f;
if (IsPlaying())
{
groupW = ImGui::CalcTextSize(pauseLabel).x +
ImGui::CalcTextSize("Stop").x +
4.0f * st.FramePadding.x + st.ItemSpacing.x;
}
else
{
groupW = ImGui::CalcTextSize("Play").x + 2.0f * st.FramePadding.x;
}
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - groupW) * 0.5f);
if (IsPlaying())
{
// Pause/Resume:暂停中亮黄常驻(UE 按下态),运行中暗黄
ImGui::PushStyleColor(ImGuiCol_Button,
IsPaused() ? ImVec4(0.85f, 0.62f, 0.12f, 1.0f)
: ImVec4(0.55f, 0.42f, 0.10f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
IsPaused() ? ImVec4(0.92f, 0.70f, 0.18f, 1.0f)
: ImVec4(0.65f, 0.50f, 0.14f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.95f, 0.76f, 0.24f, 1.0f));
if (ImGui::Button(pauseLabel))
{
TogglePause();
}
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(IsPaused() ? "Resume simulation"
: "Pause simulation");
}
ImGui::SameLine();
// Stop:橙红(沿用视口 PLAYING 提示色系)
ImGui::PushStyleColor(ImGuiCol_Button,
ImVec4(0.75f, 0.45f, 0.10f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.85f, 0.55f, 0.15f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.90f, 0.62f, 0.20f, 1.0f));
if (ImGui::Button("Stop"))
{
TogglePlay();
}
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Stop and restore editor scene (Ctrl+P)");
}
}
else
{
// Play:绿(UE Play 按钮语义)
ImGui::PushStyleColor(ImGuiCol_Button,
ImVec4(0.22f, 0.48f, 0.26f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.28f, 0.58f, 0.32f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.34f, 0.66f, 0.38f, 1.0f));
if (ImGui::Button("Play"))
{
TogglePlay();
}
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Play in editor (Ctrl+P)");
}
}
ImGui::PopStyleVar();
}
void Editor::DrawDockSpace()
{
const ImGuiID dockspaceId = ImGui::GetID("EditorDockSpace");
// 默认布局:仅当 dock node 不存在时构建一次
//(imgui.ini 已保存过布局 → DockBuilderGetNode 非空 → 跳过)
if (!m_LayoutBuilt && ImGui::DockBuilderGetNode(dockspaceId) == nullptr)
{
m_LayoutBuilt = true;
ImGui::DockBuilderRemoveNode(dockspaceId);
ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspaceId, ImGui::GetMainViewport()->Size);
// UE5 出厂布局:Details 右列通栏 → Content Browser 底部通栏
//(左+中)→ Outliner 左列 → 剩余全给 Viewport(Stats 作其 tab)
ImGuiID dockRight = 0, dockBottom = 0, dockLeft = 0, dockCenter = 0;
ImGui::DockBuilderSplitNode(dockspaceId, ImGuiDir_Right, 0.26f,
&dockRight, &dockCenter);
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Down, 0.30f,
&dockBottom, &dockCenter);
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Left, 0.22f,
&dockLeft, &dockCenter);
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
ImGui::DockBuilderDockWindow("Controller Editor", dockRight); // Details 旁 tab
ImGui::DockBuilderFinish(dockspaceId);
}
ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f),
ImGuiDockNodeFlags_None);
}
void Editor::DrawQuitModal()
{
if (m_ShowQuitConfirm)
{
ImGui::OpenPopup("Should quit?");
m_ShowQuitConfirm = false;
}
ImGui::SetNextWindowPos(
ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Should quit?", nullptr,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("Quit the application?");
ImGui::Separator();
if (ImGui::Button("Yes", ImVec2(120, 0)))
{
m_QuitConfirmed = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("No", ImVec2(120, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (m_QuitConfirmed)
{
m_QuitConfirmed = false;
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);
if (Serialization::SceneSerializer::Serialize(*m_Ctx.scene, *m_Ctx.assets,
path))
{
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;
}
if (!Serialization::SceneSerializer::Deserialize(*m_Ctx.scene,
*m_Ctx.assets, path))
{
ENGINE_ERROR("Editor: failed to load scene '{}'", path);
return;
}
ENGINE_INFO("Editor: scene loaded from '{}'", path);
// 旧实体已销毁:清选中(连带清描边,见 SelectEntity)
SelectEntity(entt::null);
}
+166
View File
@@ -0,0 +1,166 @@
//
// Editor:docking 编辑器骨架。
//
// 职责:
// - 顶栏菜单(File/View)+ 全屏 DockSpace + 首帧默认布局(DockBuilder)
// - 实体选中状态(entt::entity,Hierarchy 写 / Inspector 读)
// - 退出确认模态框(窗口关闭 → RequestQuit → Yes 时 Close)
//
// 面板窗口名(Outliner/Details/Viewport/Stats/Content Browser)即
// docking 身份,imgui.ini 持久化布局;默认布局只在 dock node 为空时
// 构建一次(旧名 ini 由 Init 一次性迁移删除,见 Editor.cpp)。
//
#ifndef HELLOVULKAN_EDITOR_H
#define HELLOVULKAN_EDITOR_H
#include "EditorContext.h"
#include "panels/ViewportPanel.h" // ViewportInputState(成员按值持有)
#include <Engine/Core/Types.h> // u32(SelectEntity 位模式转换)
#include <Engine/Core/GameFlow.h> // PIE 状态权威(IsPlaying 直查)
#include <entt/entt.hpp>
#include <memory>
#include <optional>
class HierarchyPanel;
class InspectorPanel;
class ViewportPanel;
class StatsPanel;
class LogPanel;
class GamepadPanel;
class AssetBrowserPanel;
class MaterialEditorPanel;
class ControllerEditorPanel;
class Editor {
public:
Editor();
~Editor();
Editor(const Editor&) = delete;
Editor& operator=(const Editor&) = delete;
// 注入上下文(OnInit 阶段,指针必须全部有效)
void Init(const EditorContext& ctx);
// 每帧 UI 构建(OnImGuiRender 唯一入口)
void Draw();
// ---- 输入归属判定(Window 层输入捕获谓词;UE/Unity 的
// "viewport 持焦才拥有输入" 语义,ImGui 侧经自身后端
// 链式回调永远正常收输入)----
// 键盘:编辑器形态 = Viewport 持焦且不在文本输入中;游戏形态
// (Fullscreen)只在文本输入中让位(打字不触发快捷键)
bool BlockingKeyboard() const;
// 鼠标:编辑器形态 = 悬停 Viewport 才放行;Gizmo 拖拽中一律拦截
// (抓杆时相机不能吃鼠标);游戏形态沿用 WantCaptureMouse
bool BlockingMouse() const;
// 请求退出(弹确认框;窗口关闭事件走这里)
void RequestQuit() { m_ShowQuitConfirm = true; }
// ---- PIE(Play-In-Editor,UE 主工具栏语义)----
// 状态权威 = GameFlow(EditorContext::flow);编辑器只是转换的
// 发起者(Request*)与监听器(快照/恢复挂在 →Loading/→Stopped)
bool IsPlaying() const
{
return m_Ctx.flow &&
(m_Ctx.flow->IsIn(Engine::GameFlowState::Running) ||
m_Ctx.flow->IsIn(Engine::GameFlowState::Paused));
}
bool IsPaused() const
{
return m_Ctx.flow &&
m_Ctx.flow->IsIn(Engine::GameFlowState::Paused);
}
// Play:RequestStart → →Loading 监听器做快照 + 切 gameView(Viewport
// 内嵌 primary 相机渲染,UE PIE 语义;不切全屏)→ 同步加载完成
// CompleteLoading → Running 解冻;Stop:RequestStop → →Stopped 监听器
// 快照原子换血恢复 → 回编辑器渲染。编辑器态默认冻结(GameFlow 构造
// 即 timeScale=0),Play 解冻由 GameFlow 权威处理
void TogglePlay();
// PIE 暂停切换(UE Pause 语义):Running ↔ Paused(timeScale 由
// GameFlow 权威处理)。运行态才有意义(编辑器态本来就冻结,
// Pause 按钮不出现)
void TogglePause();
private:
// GameFlow 监听器(Init 注册):编辑器快照/恢复挂在此处——
// GameFlow 不知场景,转换的副作用由消费端自己挂
void OnFlowEnterLoading(); // →Loading:快照 + 切 gameView + 同步完成加载
void OnFlowEnterStopped(); // →Stopped:快照换血恢复 + 回编辑器渲染
// ---- 选中状态 ----
// 同步描边:Renderer 落帧后 SelectionMask/Outline 只画选中实体。
// nullopt = 清选中——实体 0 号(场景首个创建)位模式 = 0,
// 不能拿位值兼作哨兵("第一个模型无描边"的根因)
void SelectEntity(entt::entity e)
{
m_Selected = e;
if (m_Ctx.renderer) {
m_Ctx.renderer->SetSelectedEntity(
e == entt::null
? std::optional<Engine::u32>{}
: std::optional<Engine::u32>(
static_cast<Engine::u32>(entt::to_integral(e))));
}
}
entt::entity GetSelected() const { return m_Selected; }
private:
void DrawMenuBar(); // host 窗口内顶栏(File/View 菜单)
void DrawToolbar(); // 菜单栏下方主工具栏(Play/Pause/Stop 居中)
void DrawDockSpace(); // 全屏 host + dockspace + 默认布局
void DrawQuitModal(); // 退出确认模态框
void DrawSceneModals(); // 保存/加载路径模态框(共用一个路径输入)
void DoSaveScene(); // 模态确认动作:目录创建 + 序列化落盘
void DoLoadScene(); // 模态确认动作:反序列化 + 选中清空 + 环境恢复 + 回调
EditorContext m_Ctx{};
ViewportInputState m_VpInput{}; // Viewport 面板 hover/focus(每帧刷新)
GizmoState m_Gizmo{}; // 视口操纵杆状态(op/拖拽中)
std::unique_ptr<HierarchyPanel> m_Hierarchy;
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;
entt::entity m_Selected{entt::null};
// 面板显隐(View 菜单)
bool m_ShowHierarchy = true;
bool m_ShowInspector = true;
bool m_ShowViewport = true;
bool m_ShowStats = true;
bool m_ShowLog = true;
bool m_ShowAssetBrowser = true;
bool m_ShowMaterialEditor = true;
// 默认布局只建一次(imgui.ini 已有布局则跳过)
bool m_LayoutBuilt = false;
// 退出确认链
bool m_ShowQuitConfirm = false;
bool m_QuitConfirmed = false;
// 场景保存/加载(File 菜单 → 路径模态框;无文件对话框库,InputText 手输)
bool m_ShowSaveModal = false;
bool m_ShowLoadModal = false;
char m_ScenePathBuf[256] = "scenes/sandbox.scene";
// PIE 镜像态(面板只读指针消费;权威 = GameFlow,Draw 开头同步)
bool m_PlayingMirror = false;
bool m_PausedMirror = false;
std::string m_PlaySnapshot;
};
#endif //HELLOVULKAN_EDITOR_H
+75
View File
@@ -0,0 +1,75 @@
//
// EditorColors.h:编辑器面板配色的单一事实来源。
//
// 资产浏览器条目、Outliner 实体名/组件徽标的着色集中在此:面板不再
// 各自散落硬编码 ImVec4。ImVec4 构造为 constexpr(imgui.h),故可用
// inline constexpr 常量。
//
#ifndef HELLOVULKAN_EDITORCOLORS_H
#define HELLOVULKAN_EDITORCOLORS_H
#include "AssetKind.h"
#include <imgui.h>
namespace EditorColors {
// ---- 资产类别色(列表标签 / 名称 / 网格瓦片 / 选中高亮)----
inline constexpr ImVec4 kModel = ImVec4(0.36f, 0.62f, 0.90f, 1.0f); // 蓝
inline constexpr ImVec4 kTexture = ImVec4(0.34f, 0.78f, 0.52f, 1.0f); // 绿
inline constexpr ImVec4 kScene = ImVec4(0.62f, 0.50f, 0.90f, 1.0f); // 紫
inline constexpr ImVec4 kMaterial = ImVec4(0.92f, 0.60f, 0.25f, 1.0f); // 橙
inline constexpr ImVec4 kAnim = ImVec4(0.30f, 0.76f, 0.76f, 1.0f); // 青
inline constexpr ImVec4 kPrefab = ImVec4(0.42f, 0.70f, 0.95f, 1.0f); // 亮蓝
inline constexpr ImVec4 kOther = ImVec4(0.72f, 0.72f, 0.75f, 1.0f); // 灰
inline constexpr ImVec4 kFolder = ImVec4(0.88f, 0.74f, 0.34f, 1.0f); // 黄
inline ImVec4 AssetColor(EditorAsset::AssetKind kind)
{
switch (kind)
{
case EditorAsset::AssetKind::Model: return kModel;
case EditorAsset::AssetKind::Texture: return kTexture;
case EditorAsset::AssetKind::Scene: return kScene;
case EditorAsset::AssetKind::Material: return kMaterial;
case EditorAsset::AssetKind::AnimClip: return kAnim;
case EditorAsset::AssetKind::Prefab: return kPrefab;
default: return kOther;
}
}
// 向白提亮(保可读性);t = 0 原色,1 全白
inline ImVec4 Lighten(const ImVec4& c, float t)
{
return ImVec4(c.x + (1.0f - c.x) * t, c.y + (1.0f - c.y) * t,
c.z + (1.0f - c.z) * t, c.w);
}
// 名称色:类别色提亮(行/瓦片名用)
inline ImVec4 AssetNameColor(EditorAsset::AssetKind kind)
{
return Lighten(AssetColor(kind), 0.40f);
}
// 选中/hover 高亮底色:类别色压暗(ImGuiCol_Header/HeaderHovered)
inline ImVec4 AssetHighlightColor(EditorAsset::AssetKind kind)
{
return ImVec4(AssetColor(kind).x * 0.42f, AssetColor(kind).y * 0.42f,
AssetColor(kind).z * 0.42f, 1.0f);
}
// ---- Outliner 组件徽标色 ----
inline constexpr ImVec4 kMesh = ImVec4(0.36f, 0.72f, 0.95f, 1.0f);
inline constexpr ImVec4 kAnimB = ImVec4(0.95f, 0.72f, 0.35f, 1.0f);
inline constexpr ImVec4 kCamera = ImVec4(0.70f, 0.55f, 0.95f, 1.0f);
inline constexpr ImVec4 kLight = ImVec4(0.96f, 0.86f, 0.36f, 1.0f);
inline constexpr ImVec4 kPhysics = ImVec4(0.45f, 0.85f, 0.55f, 1.0f);
inline constexpr ImVec4 kScript = ImVec4(0.90f, 0.55f, 0.76f, 1.0f);
inline constexpr ImVec4 kSky = ImVec4(0.46f, 0.80f, 0.92f, 1.0f);
// 无组件/兜底实体名色(近白)
inline constexpr ImVec4 kEntityNeutral = ImVec4(0.86f, 0.86f, 0.90f, 1.0f);
} // namespace EditorColors
#endif //HELLOVULKAN_EDITORCOLORS_H
+51
View File
@@ -0,0 +1,51 @@
//
// EditorContext:Editor 层与 SandboxApp 之间的桥接上下文。
//
// Editor 是 App 层代码(不属于引擎),它需要的引擎门面与沙盒能力
// 全部经此聚合注入——面板只认识 EditorContext,不反向依赖 main.cpp。
//
#ifndef HELLOVULKAN_EDITORCONTEXT_H
#define HELLOVULKAN_EDITORCONTEXT_H
#include <Engine/Core/Application.h>
#include <Engine/Core/GameFlow.h>
#include <Engine/ECS/Scene.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h>
#include <Engine/Renderer/SceneTypes.h> // kDisplayViewport / kDisplayDirect
#include <entt/entt.hpp>
class MaterialEditorPanel; // 面板间联动通道(Details 双击槽位 / Content
// Browser 双击 .material → 打开材质编辑器)
class ControllerEditorPanel; // Details Controller 槽 Edit → 打开控制器编辑器
// 沙盒特有状态(面板可选读取,空指针 = 该面板区块不显示)
struct EditorContext {
// 引擎门面
Engine::Renderer *renderer = nullptr;
Engine::ECS::Scene *scene = nullptr;
Engine::Assets::AssetManager *assets = nullptr;
Engine::Application *app = nullptr;
// 游戏流状态机(PIE 状态权威;Editor::Init 注册快照/恢复监听)
Engine::GameFlow *flow = nullptr;
// 编辑器相机(Scene view 专用,归 Renderer 持有——与场景相机实体
// 无关;面板经此做 F 焦点/Gizmo 视图/对齐跳转)
Engine::EditorCamera *editorCamera = nullptr;
// 材质编辑器面板(Editor 持有;指派/编辑分离的联动入口)
MaterialEditorPanel *materialEditor = nullptr;
// 控制器编辑器面板(Editor 持有;指派/编辑分离的联动入口)
ControllerEditorPanel *controllerEditor = nullptr;
// 当前选中实体(Controller 编辑器 Preview 的驱动目标)
entt::entity *selected = nullptr;
// PIE 运行态(Play-In-Editor):Editor 持有,只读指针。
// 面板门控用——运行态禁 Gizmo/编辑器热键,渲染源已是 primary 相机
const bool *playState = nullptr;
// PIE 暂停态(timeScale=0):Editor 持有,只读指针(视口 PAUSED 提示)
const bool *pausedState = nullptr;
};
#endif //HELLOVULKAN_EDITORCONTEXT_H
+170
View File
@@ -0,0 +1,170 @@
//
// MaterialAssetUtil.h:材质资产编辑共用工具(Inspector / Material Editor
// 面板共享)。
//
// 单一事实来源(原 InspectorPanel.cpp 匿名 ns 实现上提):
// - .material 文件枚举缓存(inline 变量跨 TU 唯一——面板各自创建
// Override 后缓存失效即时可见)
// - Create Override:内建材质快照 → assets/materials/*.material →
// GUID 登记 → 同步加载 → 挂实体覆盖表
//
#ifndef HELLOVULKAN_MATERIALASSETUTIL_H
#define HELLOVULKAN_MATERIALASSETUTIL_H
#include "AssetKind.h" // EditorAsset::IsTexturePath(纹理扩展名单一事实来源)
#include <Engine/Assets/AssetManager.h>
#include <Engine/Core/Log.h>
#include <Engine/ECS/Components.h>
#include <Engine/Renderer/MaterialAsset.h>
#include <Engine/Renderer/Model.h>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <string>
#include <vector>
namespace EditorMaterialUtil {
namespace fs = std::filesystem;
// .material 文件枚举(会话级缓存 + 脏标)。inline 变量 = 所有 TU
// 共享同一份(Create Override 新建文件后置脏重扫)
inline std::vector<std::string> s_MaterialFiles;
inline bool s_MaterialFilesDirty = true;
inline void InvalidateMaterialFileList() { s_MaterialFilesDirty = true; }
// 递归收集 assets/ 下纹理文件(材质编辑器纹理槽选择器;纹理文件
// 不被编辑器创建,无需脏标)。会话级缓存
inline const std::vector<std::string>& AssetTextureFiles()
{
static std::vector<std::string> s_Files;
static bool s_Cached = false;
if (!s_Cached)
{
s_Cached = true;
if (fs::exists("assets"))
{
for (const auto& entry :
fs::recursive_directory_iterator("assets"))
{
if (!entry.is_regular_file())
{
continue;
}
if (EditorAsset::IsTexturePath(entry.path()))
{
s_Files.push_back(entry.path().generic_string());
}
}
std::sort(s_Files.begin(), s_Files.end());
}
}
return s_Files;
}
inline const std::vector<std::string>& AssetMaterialFiles()
{
if (s_MaterialFilesDirty)
{
s_MaterialFilesDirty = false;
s_MaterialFiles.clear();
if (fs::exists("assets"))
{
for (const auto& entry :
fs::recursive_directory_iterator("assets"))
{
if (!entry.is_regular_file())
{
continue;
}
std::string ext = entry.path().extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c)
{ return std::tolower(c); });
if (ext == ".material")
{
s_MaterialFiles.push_back(
entry.path().generic_string());
}
}
std::sort(s_MaterialFiles.begin(), s_MaterialFiles.end());
}
}
return s_MaterialFiles;
}
// Create Override:内建材质快照 → assets/materials/<model>_<mesh>.material
// → GUID 登记 → 同步加载 → 挂实体覆盖表。失败返回空串(不挂槽)。
// 数据段 = 内建材质只读快照(纹理路径 + 因子 + 透明语义),此后
// 该资产独立演化,内建材质不受影响
inline std::string CreateMaterialOverride(
const std::string& modelPath,
const Engine::MeshMaterialInfo& info,
Engine::u32 meshIdx,
Engine::ECS::MeshRendererComponent& mr,
Engine::Assets::AssetManager& assets)
{
// 目标文件名:<modelStem>_<meshName|idx>.material;mesh 名非
// [A-Za-z0-9] 字符折叠 '_',空名回退下标;重名追加 _2/_3/...
std::string stem = fs::path(modelPath).stem().string();
std::string leaf = info.name;
if (leaf.empty())
{
leaf = std::to_string(meshIdx);
}
for (char& c : leaf)
{
if (!std::isalnum(static_cast<unsigned char>(c)))
{
c = '_';
}
}
const fs::path dir = "assets/materials";
std::error_code ec;
fs::create_directories(dir, ec);
if (ec)
{
ENGINE_WARN("MaterialUtil: cannot create {}: {}", dir.string(),
ec.message());
return {};
}
std::string base = stem + "_" + leaf;
std::string rel = (dir / (base + ".material")).generic_string();
for (int n = 2; fs::exists(rel); ++n)
{
rel = (dir / (base + "_" + std::to_string(n) + ".material"))
.generic_string();
}
Engine::MaterialAssetData data;
data.texPaths = info.texPaths;
data.params = info.params;
data.transparent = info.transparent;
if (!Engine::MaterialAsset::Save(rel, data))
{
ENGINE_WARN("MaterialUtil: failed to save material override '{}'",
rel);
return {};
}
assets.GetOrCreateGUID(rel);
if (!assets.LoadMaterialAsset(rel).Valid())
{
ENGINE_WARN("MaterialUtil: override '{}' saved but failed to load",
rel);
return {};
}
mr.materialOverrides[meshIdx] = rel;
InvalidateMaterialFileList(); // 新文件进下拉列表
return rel;
}
} // namespace EditorMaterialUtil
#endif //HELLOVULKAN_MATERIALASSETUTIL_H
+341
View File
@@ -0,0 +1,341 @@
//
// ThumbnailCache.cpp:缩略图解碼 + 降采样 + ImGui 用户纹理上传 + LRU 淘汰。
//
// 生命周期(依赖 vendored ImGui 1.93.0-WIP 语义,与后端/主循环逐帧对齐):
// 帧 N :Request 入队 → BeginFrame 解码 → ImTextureData::Create +
// RegisterUserTexture(Status=WantCreate,TexID=Invalid)
// 帧 N 末:UpdateTexturesEndFrame 把用户纹理并入 PlatformIO.Textures;
// ImGui_ImplVulkan_RenderDrawData 内 UpdateTexture 建 VkImage +
// 描述符,置 TexID、Status=OK
// 帧 N+1:BeginFrame 见 TexID != Invalid → Ready(当帧注册、次帧出图)
// 淘汰 :WantDestroyNextFrame=true → 后端 UnusedFrames>=ImageCount 时
// 销毁并清 BackendUserData → 下一帧见 Destroyed/无 BackendUserData
// → UnregisterUserTexture + IM_DELETE
// 关机 :本单例是进程级 function-local static,析构晚于 main 内的
// SandboxApp(含 ImGuiLayer)——ImGui_ImplVulkan_Shutdown 已对
// RefCount==1 的用户纹理统一释放 GPU;析构仅 IM_DELETE(只
// IM_FREE(Pixels),不触任何已销毁的 ImGui 上下文)。
//
#include "ThumbnailCache.h"
#include <imgui_internal.h> // RegisterUserTexture / UnregisterUserTexture
#include <stb/stb_image.h>
#include <algorithm>
#include <cstring>
namespace {
constexpr int kThumbMaxDim = 128; // 缩略图最长边
constexpr size_t kMaxEntries = 256; // 缓存上限(≤ 描述符池)
constexpr int kMaxDecodesPerFrame = 2; // 每帧最多解码张数
constexpr long long kMaxSourcePixels = 4096ll * 4096ll; // 超大源图跳过
constexpr int kReadyTimeoutFrames = 3; // 上传超时 → Failed
// box filter 等比降采样(RGBA8):输出像素在源块内最多取
// kMaxSamplesPerAxis² 个采样点(按步长抽样)。大图降采样时避免对
// 整块(如 4096² → 128² 时每像素 1024 次读)全量累加——那是单张
// 百毫秒级的卡顿源
void ResampleBoxRGBA8(const unsigned char* src, int sw, int sh,
unsigned char* dst, int dw, int dh)
{
constexpr int kMaxSamplesPerAxis = 8;
for (int y = 0; y < dh; ++y)
{
const int sy0 = y * sh / dh;
int sy1 = (y + 1) * sh / dh;
if (sy1 <= sy0) sy1 = sy0 + 1;
const int syStep = std::max(
1, (sy1 - sy0 + kMaxSamplesPerAxis - 1) / kMaxSamplesPerAxis);
for (int x = 0; x < dw; ++x)
{
const int sx0 = x * sw / dw;
int sx1 = (x + 1) * sw / dw;
if (sx1 <= sx0) sx1 = sx0 + 1;
const int sxStep = std::max(
1,
(sx1 - sx0 + kMaxSamplesPerAxis - 1) / kMaxSamplesPerAxis);
unsigned int r = 0, g = 0, b = 0, a = 0, n = 0;
for (int sy = sy0; sy < sy1; sy += syStep)
{
const unsigned char* row =
src + static_cast<size_t>(sy) * sw * 4;
for (int sx = sx0; sx < sx1; sx += sxStep)
{
const unsigned char* p =
row + static_cast<size_t>(sx) * 4;
r += p[0]; g += p[1]; b += p[2]; a += p[3]; ++n;
}
}
unsigned char* q = dst + (static_cast<size_t>(y) * dw + x) * 4;
q[0] = static_cast<unsigned char>(r / n);
q[1] = static_cast<unsigned char>(g / n);
q[2] = static_cast<unsigned char>(b / n);
q[3] = static_cast<unsigned char>(a / n);
}
}
}
} // namespace
EditorThumbnailCache& EditorThumbnailCache::Get()
{
static EditorThumbnailCache s_Cache;
return s_Cache;
}
EditorThumbnailCache::~EditorThumbnailCache()
{
// 仅回收 CPU 侧(见文件头注释)。勿调 UnregisterUserTexture——
// 此刻 ImGui 上下文已随 ImGuiLayer 销毁
for (auto& [path, e] : m_Entries)
{
if (e.tex) IM_DELETE(e.tex);
}
for (ImTextureData* tex : m_Retire) IM_DELETE(tex);
}
void EditorThumbnailCache::BeginFrame()
{
++m_Frame;
DrainRetired();
// 就绪推进:注册后等后端上传(次帧可见)
for (auto& [path, e] : m_Entries)
{
if (e.state != ThumbView::State::Pending || !e.tex) continue;
++e.pendingFrames;
if (e.tex->TexID != ImTextureID_Invalid)
{
e.state = ThumbView::State::Ready;
}
else if (e.pendingFrames > kReadyTimeoutFrames &&
e.tex->Status == ImTextureStatus_Destroyed)
{
e.state = ThumbView::State::Failed;
}
}
// 限量解码(未解码 = tex 为空)
int decodes = 0;
while (decodes < kMaxDecodesPerFrame && !m_Queue.empty())
{
const std::string p = m_Queue.front();
m_Queue.pop_front();
auto it = m_Entries.find(p);
if (it == m_Entries.end()) continue; // 已被淘汰
if (it->second.tex) continue; // 已解码(防御)
DecodeOne(p, it->second);
++decodes;
}
// 超上限 → 淘汰最久未用
while (m_Entries.size() > kMaxEntries)
{
if (!RetireOne()) break; // 全在用:本帧放弃
}
}
ThumbView EditorThumbnailCache::Request(const std::string& assetPath)
{
ThumbView v;
if (assetPath.empty())
{
v.state = ThumbView::State::Failed;
return v;
}
auto it = m_Entries.find(assetPath);
if (it == m_Entries.end())
{
Entry e;
e.lastUseFrame = m_Frame;
m_Entries.emplace(assetPath, e);
m_Queue.push_back(assetPath);
return v; // Pending
}
Entry& e = it->second;
e.lastUseFrame = m_Frame;
v.state = e.state;
v.w = e.w;
v.h = e.h;
if (e.state == ThumbView::State::Ready && e.tex)
{
v.tex = e.tex->GetTexRef();
}
return v;
}
void EditorThumbnailCache::UploadRGBA8(const unsigned char* src, int sw, int sh,
Entry& e)
{
// 等比缩放到最长边 ≤ kThumbMaxDim
int dw = sw, dh = sh;
if (sw >= sh && sw > kThumbMaxDim)
{
dw = kThumbMaxDim;
dh = std::max(1, sh * kThumbMaxDim / sw);
}
else if (sh > sw && sh > kThumbMaxDim)
{
dh = kThumbMaxDim;
dw = std::max(1, sw * kThumbMaxDim / sh);
}
ImTextureData* tex = IM_NEW(ImTextureData)();
tex->Create(ImTextureFormat_RGBA32, dw, dh);
if (dw == sw && dh == sh)
{
std::memcpy(tex->GetPixels(), src, static_cast<size_t>(dw) * dh * 4);
}
else
{
ResampleBoxRGBA8(src, sw, sh,
static_cast<unsigned char*>(tex->GetPixels()), dw, dh);
}
ImGui::RegisterUserTexture(tex);
tex->SetStatus(ImTextureStatus_WantCreate);
e.tex = tex;
e.w = dw;
e.h = dh;
e.state = ThumbView::State::Pending;
e.pendingFrames = 0;
}
void EditorThumbnailCache::DecodeOne(const std::string& path, Entry& e)
{
int sw = 0, sh = 0, ch = 0;
if (!stbi_info(path.c_str(), &sw, &sh, &ch) || sw <= 0 || sh <= 0 ||
static_cast<long long>(sw) * sh > kMaxSourcePixels)
{
e.state = ThumbView::State::Failed;
return;
}
unsigned char* src = stbi_load(path.c_str(), &sw, &sh, &ch, 4);
if (!src)
{
e.state = ThumbView::State::Failed;
return;
}
UploadRGBA8(src, sw, sh, e);
stbi_image_free(src);
e.lastUseFrame = m_Frame;
}
ThumbView EditorThumbnailCache::Peek(const std::string& assetPath)
{
ThumbView v;
auto it = m_Entries.find(assetPath);
if (it == m_Entries.end())
{
v.state = ThumbView::State::Failed; // 缺省 = 未注入
return v;
}
Entry& e = it->second;
e.lastUseFrame = m_Frame;
v.state = e.state;
v.w = e.w;
v.h = e.h;
if (e.state == ThumbView::State::Ready && e.tex)
{
v.tex = e.tex->GetTexRef();
}
return v;
}
void EditorThumbnailCache::Invalidate(const std::string& assetPath)
{
auto it = m_Entries.find(assetPath);
if (it == m_Entries.end()) return;
if (it->second.tex)
{
// 交回回收流程(与 LRU 淘汰同一路径:WantDestroyNextFrame →
// 后端释放 → DrainRetired 注销 + 回收)
it->second.tex->WantDestroyNextFrame = true;
m_Retire.push_back(it->second.tex);
}
m_Entries.erase(it);
}
unsigned long long EditorThumbnailCache::BakeKeyForPath(const std::string& path)
{
auto it = m_PathToBakeKey.find(path);
if (it != m_PathToBakeKey.end()) return it->second;
const unsigned long long key = ++m_BakeKeyCounter;
m_PathToBakeKey.emplace(path, key);
m_BakeKeyToPath.emplace(key, path);
return key;
}
void EditorThumbnailCache::CompleteBake(unsigned long long key,
const unsigned char* rgba, int w, int h)
{
auto it = m_BakeKeyToPath.find(key);
if (it == m_BakeKeyToPath.end()) return; // 已注销(重复回读)
const std::string path = it->second;
m_BakeKeyToPath.erase(it);
m_PathToBakeKey.erase(path);
InjectRGBA8(path, rgba, w, h);
}
void EditorThumbnailCache::InjectRGBA8(const std::string& path,
const unsigned char* rgba, int w, int h)
{
if (!rgba || w <= 0 || h <= 0) return;
Entry& e = m_Entries[path]; // 材质路径不经解码队列,缺省即新建
if (e.tex) return; // 已有纹理(重复注入):忽略
UploadRGBA8(rgba, w, h, e);
e.lastUseFrame = m_Frame;
}
bool EditorThumbnailCache::RetireOne()
{
auto worst = m_Entries.end();
for (auto it = m_Entries.begin(); it != m_Entries.end(); ++it)
{
if (it->second.lastUseFrame == m_Frame) continue; // 本帧在用
if (worst == m_Entries.end() ||
it->second.lastUseFrame < worst->second.lastUseFrame)
{
worst = it;
}
}
if (worst == m_Entries.end()) return false;
if (worst->second.tex)
{
worst->second.tex->WantDestroyNextFrame = true;
m_Retire.push_back(worst->second.tex);
}
m_Entries.erase(worst);
return true;
}
void EditorThumbnailCache::DrainRetired()
{
for (size_t i = 0; i < m_Retire.size();)
{
ImTextureData* tex = m_Retire[i];
// 后端销毁后清 BackendUserData 并置 Destroyed → 可安全回收
if (tex->BackendUserData == nullptr &&
tex->Status == ImTextureStatus_Destroyed)
{
ImGui::UnregisterUserTexture(tex);
IM_DELETE(tex);
m_Retire[i] = m_Retire.back();
m_Retire.pop_back();
}
else
{
++i;
}
}
}
+94
View File
@@ -0,0 +1,94 @@
//
// ThumbnailCache.h:编辑器缩略图缓存(Content Browser / 材质纹理槽共用)。
//
// 逐帧限量 CPU 解码 + box 降采样 → ImGui 用户纹理(ImTextureData +
// RegisterUserTexture)→ 后端建/传 VkImage。零引擎改动。
//
// 生命周期(详见 .cpp 注释):进程级静态单例,析构晚于 ImGuiLayer,
// CPU 侧由静态析构回收、GPU 侧由 ImGui_ImplVulkan_Shutdown 回收。
//
#ifndef HELLOVULKAN_THUMBNAILCACHE_H
#define HELLOVULKAN_THUMBNAILCACHE_H
#include <imgui.h> // ImTextureData / ImTextureRef
#include <deque>
#include <string>
#include <unordered_map>
#include <vector>
// 一次请求的结果:State==Ready 时 tex 可用于 ImGui::Image(tex, size)
struct ThumbView {
enum class State { Pending, Ready, Failed };
State state = State::Pending;
ImTextureRef tex{};
int w = 0; // 缩略图原始像素尺寸(按比例显示用)
int h = 0;
};
class EditorThumbnailCache {
public:
// 进程级单例(编辑器首帧 BeginFrame 时惰性构造)
static EditorThumbnailCache& Get();
// 每帧一次:Editor::Draw 开头(ImGui::NewFrame 之后、面板绘制之前)
void BeginFrame();
// 幂等请求:命中且就绪即返回;未命中则入队,本帧返回 Pending
ThumbView Request(const std::string& assetPath);
// 只读查状态(**不**触发解码入队):缺省 → Failed。材质球烘焙用——
// 材质路径不走 stbi 解码,注入前必须能区分"未注入"与"上传中"
//(Pending 表示已注入、等后端建纹理)
ThumbView Peek(const std::string& assetPath);
// 作废某条目的缓存纹理(材质重载/参数编辑后缩略图过期 → 重烘)。
// 丢弃 CPU 记录、纹理交回回收流程;随后 Peek 缺省 → Failed
void Invalidate(const std::string& assetPath);
// ---- 材质球烘焙(GPU 回读结果注入)----
// path ↔ key 幂等映射:Renderer 只接受/回传 u64 令牌,App 侧登记
unsigned long long BakeKeyForPath(const std::string& path);
// 回读结果注入:RGBA8 像素(w×h)→ 缩略图纹理(内部等比降到最长边
// ≤ kThumbMaxDim)。key 未登记则忽略
void CompleteBake(unsigned long long key, const unsigned char* rgba,
int w, int h);
EditorThumbnailCache(const EditorThumbnailCache&) = delete;
EditorThumbnailCache& operator=(const EditorThumbnailCache&) = delete;
private:
EditorThumbnailCache() = default;
~EditorThumbnailCache();
struct Entry {
ImTextureData* tex = nullptr; // 非拥有(删由回收流程)
int w = 0;
int h = 0;
ThumbView::State state = ThumbView::State::Pending;
unsigned long long lastUseFrame = 0;
int pendingFrames = 0; // 注册后等待后端上传的帧数
};
void DecodeOne(const std::string& path, Entry& e); // 解码→降采样→注册
// 源像素(RGBA8,sw×sh)→ 等比降采样 → ImTextureData 注册(解码与
// 烘焙回读注入共用同一条上传路径)
static void UploadRGBA8(const unsigned char* src, int sw, int sh, Entry& e);
void InjectRGBA8(const std::string& path, const unsigned char* rgba,
int w, int h);
bool RetireOne(); // 淘汰最久未用(返回是否淘汰)
void DrainRetired(); // 后端已释放→回收
std::unordered_map<std::string, Entry> m_Entries;
std::deque<std::string> m_Queue; // 待解码(重复请求命 entries,天然去重)
std::vector<ImTextureData*> m_Retire; // 已标 WantDestroy,等后端释放
unsigned long long m_Frame = 0;
// 材质球烘焙 path ↔ key 映射(幂等分配;回读完成即注销)
unsigned long long m_BakeKeyCounter = 0;
std::unordered_map<std::string, unsigned long long> m_PathToBakeKey;
std::unordered_map<unsigned long long, std::string> m_BakeKeyToPath;
};
#endif //HELLOVULKAN_THUMBNAILCACHE_H
+378
View File
@@ -0,0 +1,378 @@
//
// AssetBrowserPanel.cpp:项目资产浏览器实现。
//
// 条目视觉/拖拽/tooltip 单元住 AssetItemView(列表行与网格瓦片共用同一
// 套类别色 + 缩略图);本文件只负责:根切换 / 过滤 / 目录树 / 内容区
// 铺排 / 选中态 / 双击分派(打开或实例化)。
//
#include "AssetBrowserPanel.h"
#include "AssetItemView.h" // 列表行 / 网格瓦片 / 目录条目
#include "AssetKind.h" // EditorAsset::AssetKind / KindOfPath
#include "MaterialEditorPanel.h" // kMaterialDragPayload + 双击打开材质编辑器
#include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器
#include <Engine/Core/Log.h>
#include <Engine/Serialization/SceneSerializer.h>
#include <imgui.h>
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <utility>
#include <vector>
namespace {
// 固定根:下标与 Editor 无耦合,仅面板内部使用
constexpr const char* kRootNames[] = {"Assets", "Scenes"};
constexpr const char* kRootPaths[] = {"assets", "scenes"};
// 类型过滤(下标即身份):0 = All;其余对齐 AssetKind 顺序
constexpr const char* kTypeNames[] = {"All", "Models",
"Textures", "Scenes",
"Materials", "Controllers",
"Prefabs"};
bool PassFilter(int typeFilter, EditorAsset::AssetKind kind)
{
switch (typeFilter)
{
case 1: return kind == EditorAsset::AssetKind::Model;
case 2: return kind == EditorAsset::AssetKind::Texture;
case 3: return kind == EditorAsset::AssetKind::Scene;
case 4: return kind == EditorAsset::AssetKind::Material;
case 5: return kind == EditorAsset::AssetKind::AnimClip;
case 6: return kind == EditorAsset::AssetKind::Prefab;
default: return true;
}
}
// 相对路径规范化为正斜杠(AssetManager 键与场景文件统一 POSIX 风格)
std::string ToAssetPath(const std::filesystem::path& p)
{
std::string s = p.generic_string();
std::replace(s.begin(), s.end(), '\\', '/');
return s;
}
// 双击分派:材质/控制器 → 打开编辑器;模型/环境 → 缓存预热;
// 场景 → 提示走 File 菜单;预制体 → 快捷实例化(授权位姿落点)
void DispatchOpen(const EditorContext& ctx,
const std::filesystem::path& path,
EditorAsset::AssetKind kind)
{
const std::string assetPath = path.generic_string();
if (kind == EditorAsset::AssetKind::Material && ctx.materialEditor)
{
ctx.materialEditor->OpenAsset(assetPath);
}
else if (kind == EditorAsset::AssetKind::AnimClip && ctx.controllerEditor)
{
ctx.controllerEditor->Open(assetPath);
}
else if (kind == EditorAsset::AssetKind::Model && ctx.assets)
{
const auto ref = ctx.assets->LoadModel(assetPath);
ENGINE_INFO("AssetBrowser: model '{}' loaded (valid={})",
assetPath, ref.Valid());
}
else if (kind == EditorAsset::AssetKind::Texture && ctx.assets)
{
const auto ref = ctx.assets->LoadEnvironment(assetPath);
ENGINE_INFO("AssetBrowser: env '{}' loaded (valid={})",
assetPath, ref.Valid());
}
else if (kind == EditorAsset::AssetKind::Scene)
{
ENGINE_INFO("AssetBrowser: load scenes via File menu ({})",
assetPath);
}
else if (kind == EditorAsset::AssetKind::Prefab && ctx.scene && ctx.assets)
{
// 双击 = 快捷实例化(授权位姿落点,选中交给 Outliner 手动改);
// 拖拽实例化见 Outliner 落点
const auto root = Engine::Serialization::SceneSerializer::
InstantiatePrefab(*ctx.scene, *ctx.assets, assetPath);
ENGINE_INFO("AssetBrowser: prefab '{}' instantiated (valid={})",
assetPath, root != entt::null);
}
}
} // namespace
void AssetBrowserPanel::Draw(const EditorContext& ctx)
{
if (!ImGui::Begin("Content Browser"))
{
ImGui::End();
return;
}
// ---- 顶栏:根切换 + 类型过滤 + 名字过滤 ----
if (ImGui::BeginTabBar("##roots"))
{
for (int i = 0; i < 2; ++i)
{
if (ImGui::BeginTabItem(kRootNames[i]))
{
m_Root = i;
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
ImGui::SameLine();
ImGui::SetNextItemWidth(110.0f);
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 7);
ImGui::SameLine();
ImGui::SetNextItemWidth(140.0f);
ImGui::InputTextWithHint("##name", "filter", m_NameFilter,
sizeof(m_NameFilter));
ImGui::Separator();
const std::filesystem::path root(kRootPaths[m_Root]);
// 根切换 / 首帧:当前目录回根。前缀比较手写(path::starts_with
// 是 libstdc++ 11 才补齐的成员,本机 GCC 没有);补 '/' 分隔校验
// 防 "assets_x" 之类误匹配
const std::string rootStr = root.generic_string();
const std::string curStr = m_CurrentDir.generic_string();
const bool under = curStr == rootStr ||
(curStr.size() > rootStr.size() &&
curStr.compare(0, rootStr.size(), rootStr) == 0 &&
curStr[rootStr.size()] == '/');
if (m_CurrentDir.empty() || !under)
{
m_CurrentDir = root;
}
// ---- 顶栏下:面包屑(当前目录相对根路径 + 回根按钮)+ 视图切换 ----
if (ImGui::Button("Root"))
{
m_CurrentDir = root;
}
ImGui::SameLine();
std::string rel = std::filesystem::relative(m_CurrentDir, root)
.generic_string();
ImGui::TextDisabled("%s/%s", kRootNames[m_Root],
rel.empty() ? "" : rel.c_str());
// 视图切换(列表 / 网格)+ 网格瓦片尺寸
ImGui::SameLine();
const bool listMode = m_ViewMode == ViewMode::List;
if (ImGui::RadioButton("List", listMode))
{
m_ViewMode = ViewMode::List;
}
ImGui::SameLine();
if (ImGui::RadioButton("Grid", !listMode))
{
m_ViewMode = ViewMode::Grid;
}
if (m_ViewMode == ViewMode::Grid)
{
ImGui::SameLine();
ImGui::SetNextItemWidth(120.0f);
ImGui::SliderFloat("##tile", &m_TileSize, 48.0f, 160.0f, "%.0f px");
}
ImGui::Separator();
// ---- 左:目录树 | 右:当前目录内容(列表 / 网格)----
const float treeWidth = 180.0f;
ImGui::BeginChild("##tree", ImVec2(treeWidth, 0.0f),
ImGuiChildFlags_ResizeX | ImGuiChildFlags_Borders);
if (std::filesystem::exists(root))
{
// 根节点也画进树(可点击选中)
ImGui::SetNextItemOpen(true, ImGuiCond_Once);
if (ImGui::TreeNodeEx(kRootNames[m_Root],
ImGuiTreeNodeFlags_OpenOnArrow))
{
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
{
m_CurrentDir = root;
}
DrawDirTree(ctx, root);
ImGui::TreePop();
}
}
else
{
ImGui::TextDisabled("(no '%s' dir)", kRootPaths[m_Root]);
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("##files", ImVec2(0.0f, 0.0f),
ImGuiChildFlags_Borders);
if (std::filesystem::exists(m_CurrentDir))
{
DrawContent(ctx, m_CurrentDir);
}
ImGui::EndChild();
ImGui::End();
}
void AssetBrowserPanel::DrawDirTree(const EditorContext& ctx,
const std::filesystem::path& dir)
{
for (const auto& entry : std::filesystem::directory_iterator(dir))
{
if (!entry.is_directory())
{
continue;
}
const std::string name = entry.path().filename().string();
// OpenOnArrow:单击行 = 选中(列表切换),箭头 = 折叠
const bool open = ImGui::TreeNodeEx(
name.c_str(), ImGuiTreeNodeFlags_OpenOnArrow |
ImGuiTreeNodeFlags_SpanFullWidth);
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
{
m_CurrentDir = entry.path();
}
if (open)
{
DrawDirTree(ctx, entry.path());
ImGui::TreePop();
}
}
}
void AssetBrowserPanel::DrawContent(const EditorContext& ctx,
const std::filesystem::path& dir)
{
namespace fs = std::filesystem;
// 收集:目录(不受类型过滤影响)+ 过滤后的文件;目录迭代序不稳定 → 排序
std::vector<fs::path> dirs;
std::vector<std::pair<fs::path, EditorAsset::AssetKind>> files;
const std::string filter = m_NameFilter;
std::error_code ec;
for (const auto& entry : fs::directory_iterator(dir, ec))
{
if (entry.is_directory())
{
dirs.push_back(entry.path());
continue;
}
const EditorAsset::AssetKind kind =
EditorAsset::KindOfPath(entry.path());
if (!PassFilter(m_TypeFilter, kind))
{
continue;
}
if (!filter.empty())
{
const std::string name = entry.path().filename().string();
if (name.find(filter) == std::string::npos)
{
continue;
}
}
files.emplace_back(entry.path(), kind);
}
std::sort(dirs.begin(), dirs.end(),
[](const fs::path& a, const fs::path& b)
{
return a.filename().string() < b.filename().string();
});
std::sort(files.begin(), files.end(),
[](const auto& a, const auto& b)
{
return a.first.filename().string() <
b.first.filename().string();
});
// 双击打开延迟到循环外:Immediate GUI 语义——绘制循环内不改世界状态
//(prefab 实例化会改场景,边遍历边打开会让后续条目的项状态错乱)
bool pendingOpen = false;
std::filesystem::path pendingOpenPath;
EditorAsset::AssetKind pendingOpenKind = EditorAsset::AssetKind::Other;
if (m_ViewMode == ViewMode::List)
{
for (const auto& d : dirs)
{
if (EditorAssetView::DrawFolderRow(d))
{
m_CurrentDir = d;
m_SelectedPath.clear();
}
}
for (const auto& [path, kind] : files)
{
const std::string assetPath = ToAssetPath(path);
const auto res = EditorAssetView::DrawRow(
ctx, path, assetPath == m_SelectedPath);
if (res.clicked)
{
m_SelectedPath = assetPath;
}
if (res.doubleClicked)
{
pendingOpen = true;
pendingOpenPath = path;
pendingOpenKind = kind;
}
}
}
else
{
// ---- 网格视图:固定列宽表格铺排(列数按可用宽度自适应)----
const float tile = m_TileSize;
const float cellW = tile + ImGui::GetStyle().CellPadding.x * 2.0f;
int cols = static_cast<int>(ImGui::GetContentRegionAvail().x / cellW);
if (cols < 1)
{
cols = 1;
}
if (ImGui::BeginTable("##grid", cols, ImGuiTableFlags_SizingFixedFit))
{
for (int i = 0; i < cols; ++i)
{
ImGui::TableSetupColumn("##c",
ImGuiTableColumnFlags_WidthFixed,
cellW);
}
for (const auto& d : dirs)
{
ImGui::TableNextColumn();
const std::string assetPath = ToAssetPath(d);
if (EditorAssetView::DrawFolderTile(
d, tile, assetPath == m_SelectedPath))
{
m_CurrentDir = d;
m_SelectedPath.clear();
}
}
for (const auto& [path, kind] : files)
{
ImGui::TableNextColumn();
const std::string assetPath = ToAssetPath(path);
const auto res = EditorAssetView::DrawTile(
ctx, path, tile, assetPath == m_SelectedPath);
if (res.clicked)
{
m_SelectedPath = assetPath;
}
if (res.doubleClicked)
{
pendingOpen = true;
pendingOpenPath = path;
pendingOpenKind = kind;
}
}
ImGui::EndTable();
}
}
if (pendingOpen)
{
DispatchOpen(ctx, pendingOpenPath, pendingOpenKind);
}
}
+59
View File
@@ -0,0 +1,59 @@
//
// AssetBrowserPanel:项目资产浏览器(Unity Project 窗口简化版)。
//
// 左侧目录树 + 右侧文件列表(类型过滤 + 名字过滤)。与 AssetManager
// 的集成点是"双击预热":model/HDR 进缓存去重(重复双击零成本),
// .scene 提示走 File 菜单(加载含环境恢复/选中清理等流程,不在面板
// 重复)。拖拽到视口/Inspector 属后续迭代。
//
// 目录每帧扫描:项目级目录条目有限(几十量级),filesystem 迭代开销
// 可忽略;资产规模上来后再加缓存脏标。
//
#ifndef HELLOVULKAN_ASSETBROWSERPANEL_H
#define HELLOVULKAN_ASSETBROWSERPANEL_H
#include "EditorContext.h"
#include <filesystem>
#include <string>
// Prefab 资产拖拽 payload(Content Browser 拖 .prefab → Outliner 落点
// 实例化;字符串 = 资产相对路径)。对齐 kMaterialDragPayload 模式:
// 常量住源面板头
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);
private:
// 视图模式:列表(行,含内联小缩略图)/ 网格(缩略图瓦片)
enum class ViewMode { List, Grid };
// 目录树节点(递归);右侧内容区画 dir 下的目录+文件
void DrawDirTree(const EditorContext& ctx,
const std::filesystem::path& dir);
void DrawContent(const EditorContext& ctx,
const std::filesystem::path& dir);
// 固定根(下标即身份):项目资产 / 场景文件
int m_Root = 0;
// 当前浏览目录(点目录树切换;内容区显示这里的条目)
std::filesystem::path m_CurrentDir;
// 类型过滤(0 = All;其余见 kTypeNames)
int m_TypeFilter = 0;
// 名字子串过滤(空 = 不过滤)
char m_NameFilter[64] = "";
// 视图模式 + 网格瓦片尺寸(像素)
ViewMode m_ViewMode = ViewMode::List;
float m_TileSize = 96.0f;
// 当前选中条目(资产相对路径;空 = 无选中)
std::string m_SelectedPath;
};
#endif //HELLOVULKAN_ASSETBROWSERPANEL_H
+786
View File
@@ -0,0 +1,786 @@
//
// ControllerEditorPanel 实现:见头文件注释。
//
#include "ControllerEditorPanel.h"
#include <Engine/Animation/AnimationController.h>
#include <Engine/ECS/Components.h>
#include <Engine/Renderer/Model.h>
#include <imgui.h>
#include <algorithm>
#include <cstdio>
namespace {
ImVec4 WarnCol() { return ImVec4(1.0f, 0.75f, 0.2f, 1.0f); }
// 模型首个 clip 名(children/树新建时的默认引用;无模型 = 空)
std::string FirstClipName(const Engine::Model* model)
{
if (model == nullptr || model->GetClips().empty()) return "";
return model->GetClips()[0].name;
}
// clip 引用编辑:选中实体有模型 → combo(模型 clip 列表);
// 无模型 → InputText 手填(clip 缺失 = 该层 rest 兜底,不崩)
bool ClipRefEdit(const char* label, std::string& clip,
const Engine::Model* model)
{
if (model == nullptr || model->GetClips().empty())
{
char buf[128];
snprintf(buf, sizeof(buf), "%s", clip.c_str());
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::InputText(label, buf, sizeof(buf)))
{
clip = buf;
return true;
}
return false;
}
std::vector<const char*> names;
names.reserve(model->GetClips().size());
int sel = -1;
for (const auto& c : model->GetClips())
{
names.push_back(c.name.c_str());
if (c.name == clip) sel = static_cast<int>(names.size()) - 1;
}
if (sel < 0) sel = 0;
if (ImGui::Combo(label, &sel, names.data(),
static_cast<int>(names.size())))
{
clip = model->GetClips()[static_cast<size_t>(sel)].name;
return true;
}
return false;
}
} // namespace
void ControllerEditorPanel::Draw(const EditorContext& ctx)
{
if (!m_Open || m_Path.empty()) return;
Engine::Animation::AnimationController* ctrl =
ctx.assets->FindController(m_Path);
if (!ImGui::Begin("Controller Editor", &m_Open))
{
ImGui::End();
return;
}
if (ctrl == nullptr)
{
ImGui::TextColored(WarnCol(), "'%s' failed to load",
m_Path.c_str());
if (ImGui::Button("Reload"))
{
ctx.assets->ReloadController(m_Path);
}
ImGui::End();
return;
}
// ---- 头部:路径 + Save(落盘 + Reload 重编译条件)----
ImGui::TextUnformatted(m_Path.c_str());
ImGui::SameLine();
if (ImGui::Button("Save"))
{
if (ctrl->Save(m_Path))
{
ctx.assets->ReloadController(m_Path);
ctrl = ctx.assets->FindController(m_Path);
}
}
ImGui::SameLine();
ImGui::TextDisabled("edits apply live; Save writes the file");
ImGui::Separator();
DrawPreview(ctx);
ImGui::Separator();
DrawStates(ctx, ctrl);
ImGui::Separator();
DrawTransitions(ctx, ctrl);
ImGui::Separator();
DrawParams(ctx, ctrl);
ImGui::End();
}
// ---- Preview:方向盘(拖动 = SetFloat 到选中实体,摇杆回中)----
void ControllerEditorPanel::DrawPreview(const EditorContext& ctx)
{
// 选中实体的 AnimationComponent(Controller 模式才驱动)
Engine::ECS::AnimationComponent* anim = nullptr;
const Engine::Model* model = nullptr;
if (ctx.selected != nullptr && ctx.scene != nullptr &&
ctx.scene->GetRegistry().valid(*ctx.selected))
{
auto& reg = ctx.scene->GetRegistry();
if (auto* a =
reg.try_get<Engine::ECS::AnimationComponent>(*ctx.selected))
{
anim = a;
if (auto* mr =
reg.try_get<Engine::ECS::MeshRendererComponent>(
*ctx.selected))
{
if (mr->model.Valid())
{
model = ctx.assets->GetModel(mr->model);
}
}
}
}
if (anim == nullptr)
{
ImGui::TextDisabled("select an entity with an Animation component "
"to preview");
return;
}
Engine::Animation::AnimationController* ctrl =
anim->controllerPath.empty()
? nullptr
: ctx.assets->FindController(anim->controllerPath);
if (ctrl == nullptr) return;
// 面板选中状态的 2D 树(预览读出;拖动写的是实体参数)
const Engine::Animation::ControllerState* padState = nullptr;
const auto& order = ctrl->GetStateOrder();
if (m_SelState >= 0 && m_SelState < static_cast<int>(order.size()))
{
const auto& s = ctrl->GetStates().at(order[static_cast<size_t>(
m_SelState)]);
if (s.tree.type ==
Engine::Animation::BlendTree::Type::Directional2D)
{
padState = &s;
}
}
if (padState == nullptr)
{
ImGui::TextDisabled("preview: select a 2DDirectional state");
return;
}
const float S = 190.0f;
const float rad = S * 0.5f - 14.0f;
const ImVec2 org = ImGui::GetCursorScreenPos();
const ImVec2 ctr(org.x + S * 0.5f, org.y + S * 0.5f);
ImDrawList* dl = ImGui::GetWindowDrawList();
dl->AddCircleFilled(ctr, rad, IM_COL32(30, 30, 34, 255));
dl->AddCircle(ctr, rad, IM_COL32(90, 90, 100, 255), 48, 1.5f);
dl->AddLine(ImVec2(ctr.x - rad, ctr.y), ImVec2(ctr.x + rad, ctr.y),
IM_COL32(60, 60, 68, 255));
dl->AddLine(ImVec2(ctr.x, ctr.y - rad), ImVec2(ctr.x, ctr.y + rad),
IM_COL32(60, 60, 68, 255));
const auto& ch = padState->tree.children;
for (const auto& c : ch)
{
const bool isOrigin =
c.pos.x * c.pos.x + c.pos.y * c.pos.y < 1e-8f;
const ImVec2 p(ctr.x + c.pos.x * rad, ctr.y - c.pos.y * rad);
dl->AddCircleFilled(p, isOrigin ? 5.0f : 4.0f,
isOrigin ? IM_COL32(90, 140, 255, 255)
: IM_COL32(220, 220, 225, 255));
}
// mode=Direct 的实体不跑状态机——参数写进去没有任何效果,
// 直接禁用方向盘(Unity:Animator 组件没挂 Controller 时
// 参数面板不可编辑,同语义)
const bool isCtrlMode =
anim->mode == Engine::ECS::AnimationComponent::AnimMode::Controller;
if (!isCtrlMode)
{
ImGui::TextColored(WarnCol(),
"entity is in Direct mode - switch Mode to "
"Controller to preview");
return;
}
ImGui::InvisibleButton("##dirpad", ImVec2(S, S));
const auto getF = [&](const std::string& n) -> float {
auto it = anim->floatParams.find(n);
return it != anim->floatParams.end() ? it->second : 0.0f;
};
float cx = getF(padState->tree.xParam);
float cz = getF(padState->tree.zParam);
if (ImGui::IsItemActivated())
{
// 预览闭环(Unity Animator 窗口语义:选中状态即可预览该
// 状态)——实体停在别的状态时拖盘自动切到面板选中的 2D
// 树状态,否则 SetFloat 对 Single 状态完全无效
if (anim->currentState != order[static_cast<size_t>(m_SelState)])
{
anim->CrossFade(order[static_cast<size_t>(m_SelState)], 0.1f);
}
}
if (ImGui::IsItemActive())
{
const ImVec2 m = ImGui::GetIO().MousePos;
float vx = (m.x - ctr.x) / rad;
float vz = -(m.y - ctr.y) / rad;
const float len = glm::length(glm::vec2(vx, vz));
if (len > 1.0f)
{
vx /= len;
vz /= len;
}
anim->SetFloat(padState->tree.xParam, vx);
anim->SetFloat(padState->tree.zParam, vz);
cx = vx;
cz = vz;
}
else if (ImGui::IsItemDeactivatedAfterEdit())
{
anim->SetFloat(padState->tree.xParam, 0.0f);
anim->SetFloat(padState->tree.zParam, 0.0f);
cx = cz = 0.0f;
}
if (ImGui::IsItemHovered())
{
const ImVec2 m = ImGui::GetIO().MousePos;
for (const auto& c : ch)
{
const ImVec2 p(ctr.x + c.pos.x * rad, ctr.y - c.pos.y * rad);
const float dx = m.x - p.x;
const float dy = m.y - p.y;
if (dx * dx + dy * dy < 64.0f)
{
ImGui::SetTooltip("%s", c.clip.c_str());
break;
}
}
}
const ImVec2 cur(ctr.x + cx * rad, ctr.y - cz * rad);
dl->AddCircleFilled(cur, 6.0f, IM_COL32(255, 170, 60, 255));
dl->AddCircle(cur, 9.0f, IM_COL32(255, 170, 60, 160), 0, 1.5f);
// 右侧读出:当前参数 + 选中状态树的权重(运行时同一数学)
ImGui::SameLine();
ImGui::BeginGroup();
ImGui::Text("preview: %s", anim->currentState.c_str());
ImGui::Text("%.2f / %.2f", cx, cz);
std::vector<std::pair<Engine::u32, float>> ws;
Engine::Animation::EvaluateBlendTree2D(padState->tree, cx, cz, ws);
std::sort(ws.begin(), ws.end(),
[](const auto& a, const auto& b)
{ return a.second > b.second; });
for (const auto& [idx, w] : ws)
{
ImGui::Text("%s %.2f", ch[idx].clip.c_str(), w);
}
ImGui::EndGroup();
}
// ---- States:列表(entry ★)+ 选中状态详情 + children 编辑 ----
void ControllerEditorPanel::DrawStates(
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
{
if (!ImGui::TreeNodeEx("States", ImGuiTreeNodeFlags_DefaultOpen))
{
return;
}
auto& order = ctrl->StateOrderMut();
auto& states = ctrl->StatesMut();
// 选中实体模型(children 的 clip 下拉来源)
const Engine::Model* model = nullptr;
if (ctx.selected != nullptr && ctx.scene != nullptr &&
ctx.scene->GetRegistry().valid(*ctx.selected))
{
if (auto* mr =
ctx.scene->GetRegistry()
.try_get<Engine::ECS::MeshRendererComponent>(
*ctx.selected))
{
if (mr->model.Valid())
{
model = ctx.assets->GetModel(mr->model);
}
}
}
// 状态列表 + Add
int delIdx = -1;
for (int i = 0; i < static_cast<int>(order.size()); ++i)
{
const bool isEntry = order[static_cast<size_t>(i)] ==
ctrl->GetEntryState();
ImGui::PushID(i);
if (ImGui::Selectable(
(order[static_cast<size_t>(i)] +
std::string(isEntry ? " [entry]" : ""))
.c_str(),
i == m_SelState))
{
m_SelState = i;
}
ImGui::PopID();
}
{
static char s_NewName[64] = "";
ImGui::SetNextItemWidth(120.0f);
ImGui::InputText("##new_state", s_NewName, sizeof(s_NewName));
ImGui::SameLine();
if (ImGui::SmallButton("+ Add") && s_NewName[0] != '\0' &&
states.find(s_NewName) == states.end())
{
const std::string n = s_NewName;
states.emplace(n, Engine::Animation::ControllerState{n});
order.push_back(n);
m_SelState = static_cast<int>(order.size()) - 1;
s_NewName[0] = '\0';
}
}
if (m_SelState >= static_cast<int>(order.size()))
{
m_SelState = static_cast<int>(order.size()) - 1;
}
if (m_SelState < 0 || order.empty())
{
ImGui::TreePop();
return;
}
// ---- 选中状态详情 ----
const std::string selName = order[static_cast<size_t>(m_SelState)];
auto it = states.find(selName);
if (it == states.end())
{
ImGui::TreePop();
return;
}
auto& st = it->second;
ImGui::Indent();
ImGui::TextUnformatted("editing:");
ImGui::SameLine();
ImGui::TextUnformatted(selName.c_str());
if (selName != ctrl->GetEntryState() &&
ImGui::SmallButton("Set as Entry"))
{
ctrl->SetEntry(selName);
}
ImGui::SameLine();
ImGui::PushID("delstate");
if (ImGui::SmallButton("x Delete"))
{
delIdx = m_SelState;
}
ImGui::PopID();
// 重命名(级联:transitions / transitionsOnEnd / entry 引用同步)
{
static char s_Rename[64] = "";
ImGui::SetNextItemWidth(120.0f);
ImGui::InputText("##rename", s_Rename, sizeof(s_Rename));
ImGui::SameLine();
if (ImGui::SmallButton("Rename") && s_Rename[0] != '\0' &&
states.find(s_Rename) == states.end())
{
const std::string oldN = selName;
const std::string newN = s_Rename;
auto node = states.extract(oldN);
if (node)
{
node.key() = newN;
states.insert(std::move(node));
order[static_cast<size_t>(m_SelState)] = newN;
for (auto& t : ctrl->TransitionsMut())
{
if (t.from == oldN) t.from = newN;
if (t.to == oldN) t.to = newN;
}
for (auto& [n, s] : states)
{
for (auto& e : s.transitionsOnEnd)
{
if (e == oldN) e = newN;
}
}
if (ctrl->GetEntryState() == oldN)
{
ctrl->SetEntry(newN);
}
s_Rename[0] = '\0';
}
}
}
// ---- 播放分组(speed/loop/播完出口)----
if (ImGui::TreeNode("Playback"))
{
ImGui::DragFloat("speed", &st.speed, 0.05f, 0.1f, 10.0f, "%.2f");
ImGui::Checkbox("loop", &st.loop);
if (!st.loop)
{
// 播完出口列表(按序首个可用)+ 下拉追加
for (size_t ei = 0; ei < st.transitionsOnEnd.size(); ++ei)
{
ImGui::PushID(static_cast<int>(ei) + 1000);
ImGui::BulletText("%s",
st.transitionsOnEnd[ei].c_str());
ImGui::SameLine();
if (ImGui::SmallButton("x"))
{
st.transitionsOnEnd.erase(
st.transitionsOnEnd.begin() +
static_cast<long>(ei));
ImGui::PopID();
break;
}
ImGui::PopID();
}
if (!order.empty())
{
static int s_OnEndSel = 0;
s_OnEndSel = std::clamp(
s_OnEndSel, 0,
static_cast<int>(order.size()) - 1);
ImGui::SetNextItemWidth(120.0f);
ImGui::Combo(
"##onend", &s_OnEndSel,
[](void* d, int i) -> const char* {
return (*static_cast<std::vector<std::string>*>(
d))[static_cast<size_t>(i)]
.c_str();
},
&order, static_cast<int>(order.size()));
ImGui::SameLine();
if (ImGui::SmallButton("+ exit"))
{
st.transitionsOnEnd.push_back(
order[static_cast<size_t>(s_OnEndSel)]);
}
}
}
ImGui::TreePop();
}
// ---- 混合树分组 ----
if (ImGui::TreeNode("Blend Tree"))
{
using BTType = Engine::Animation::BlendTree::Type;
// 树类型:Single 单 clip / 1D 阈值 / 2D 平面方向
int tt = static_cast<int>(st.tree.type);
if (ImGui::Combo("type", &tt,
"Single\0Directional 1D\0Directional 2D\0"))
{
st.tree.type = static_cast<BTType>(tt);
st.tree.children.clear();
if (st.tree.type == BTType::Single)
{
if (st.tree.clip.empty())
{
st.tree.clip = FirstClipName(model);
}
}
else
{
// 默认一个原点 child(clip 取模型首个;无模型 = 手填)
st.tree.children.push_back(
{FirstClipName(model), glm::vec2{0.0f, 0.0f}});
}
}
// x/z 参数绑定(float 参数下拉)
{
std::vector<const char*> pnames;
for (const auto& p : ctrl->GetParams())
{
if (p.type ==
Engine::Animation::ControllerParam::Type::Float)
{
pnames.push_back(p.name.c_str());
}
}
if (!pnames.empty())
{
if (st.tree.type != BTType::Single)
{
int xi = 0;
for (int k = 0; k < static_cast<int>(pnames.size());
++k)
{
if (st.tree.xParam ==
pnames[static_cast<size_t>(k)])
xi = k;
}
ImGui::SetNextItemWidth(140.0f);
if (ImGui::Combo("x param", &xi, pnames.data(),
static_cast<int>(pnames.size())))
{
st.tree.xParam = pnames[static_cast<size_t>(xi)];
}
}
if (st.tree.type == BTType::Directional2D)
{
int zi = 0;
for (int k = 0; k < static_cast<int>(pnames.size());
++k)
{
if (st.tree.zParam ==
pnames[static_cast<size_t>(k)])
zi = k;
}
ImGui::SetNextItemWidth(140.0f);
if (ImGui::Combo("z param", &zi, pnames.data(),
static_cast<int>(pnames.size())))
{
st.tree.zParam = pnames[static_cast<size_t>(zi)];
}
}
}
else if (st.tree.type != BTType::Single)
{
ImGui::TextColored(
WarnCol(), "need a float param for the blend axis");
}
}
if (st.tree.type == BTType::Single)
{
ClipRefEdit("clip", st.tree.clip, model);
}
else
{
// 原点点缺失警告:v 回到中心时方向层权重全被最小阈值滤
// 掉,层空 → 引擎侧 fallback 到首个有效 child 单层(不是
// T-pose),但"中心=默认动作"的直觉语义要靠原点点承载
if (st.tree.type == BTType::Directional2D)
{
bool hasOrigin = false;
for (const auto& c : st.tree.children)
{
if (c.pos.x * c.pos.x + c.pos.y * c.pos.y < 1e-8f)
hasOrigin = true;
}
if (!hasOrigin)
{
ImGui::TextColored(
WarnCol(), "! no origin child (at 0,0) - center "
"falls back to first child");
}
}
// children 表:clip + 定位 + 删除(2D = (x,y) 平面定位点;
// 1D = x 即阈值)。编辑即时生效(System 下帧直读缓存实例)
int delChild = -1;
for (int ci = 0;
ci < static_cast<int>(st.tree.children.size()); ++ci)
{
auto& c = st.tree.children[static_cast<size_t>(ci)];
ImGui::PushID(ci);
ClipRefEdit("##clip", c.clip, model);
if (st.tree.type == BTType::Directional2D)
{
ImGui::SetNextItemWidth(70.0f);
ImGui::DragFloat("##atx", &c.pos.x, 0.01f, -1.0f,
1.0f, "x %.2f");
ImGui::SameLine();
ImGui::SetNextItemWidth(70.0f);
ImGui::DragFloat("##aty", &c.pos.y, 0.01f, -1.0f,
1.0f, "y %.2f");
}
else
{
ImGui::SetNextItemWidth(120.0f);
ImGui::DragFloat("##thr", &c.pos.x, 0.05f, -10.0f,
10.0f, "thr %.2f");
}
ImGui::SameLine();
if (ImGui::SmallButton("x")) delChild = ci;
ImGui::PopID();
}
if (delChild >= 0)
{
st.tree.children.erase(
st.tree.children.begin() + delChild);
}
if (ImGui::SmallButton("+ Add Child"))
{
st.tree.children.push_back(
{FirstClipName(model),
st.tree.type == BTType::Directional2D
? glm::vec2{1.0f, 0.0f}
: glm::vec2{0.0f, 0.0f}});
}
}
ImGui::TreePop(); // Blend Tree
}
ImGui::Unindent();
if (delIdx >= 0)
{
const std::string name = order[static_cast<size_t>(delIdx)];
states.erase(name);
order.erase(order.begin() + delIdx);
auto& trs = ctrl->TransitionsMut();
for (int i = static_cast<int>(trs.size()) - 1; i >= 0; --i)
{
if (trs[static_cast<size_t>(i)].from == name ||
trs[static_cast<size_t>(i)].to == name)
{
trs.erase(trs.begin() + i);
}
}
for (auto& [n, s] : states)
{
std::erase_if(s.transitionsOnEnd,
[&](const std::string& e)
{ return e == name; });
}
if (ctrl->GetEntryState() == name)
{
ctrl->SetEntry(order.empty() ? "" : order[0]);
}
m_SelState = 0;
}
ImGui::TreePop();
}
// ---- Transitions:行编辑(from/to 下拉 + cond 字符串 Enter 编译)----
void ControllerEditorPanel::DrawTransitions(
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
{
(void)ctx;
if (!ImGui::TreeNode("Transitions")) return;
const auto& order = ctrl->GetStateOrder();
std::vector<const char*> snames;
snames.reserve(order.size() + 1);
snames.push_back("*"); // AnyState
for (const auto& n : order) snames.push_back(n.c_str());
auto stateCombo = [&](const char* label, std::string& cur) {
int sel = 0;
for (int k = 0; k < static_cast<int>(snames.size()); ++k)
{
if (cur == snames[static_cast<size_t>(k)]) sel = k;
}
ImGui::SetNextItemWidth(110.0f);
if (ImGui::Combo(label, &sel, snames.data(),
static_cast<int>(snames.size())))
{
cur = snames[static_cast<size_t>(sel)];
}
};
int delIdx = -1;
int ti = 0;
for (auto& t : ctrl->TransitionsMut())
{
ImGui::PushID(ti++);
// 行 1:来源 -> 目标(+ 删除)
stateCombo("##from", t.from);
ImGui::SameLine();
ImGui::TextUnformatted("->");
ImGui::SameLine();
stateCombo("##to", t.to);
ImGui::SameLine();
if (ImGui::SmallButton("x")) delIdx = ti - 1;
// 行 2:条件 + fade + exitTime(缩进——同一过渡的属性组)
ImGui::Indent(18.0f);
// cond:反推字符串 → Enter(DeactivatedAfterEdit)就地重编译
{
const std::string s =
t.cond.always ? "" :
Engine::Animation::AnimationController::CondToString(
t.cond);
char buf[96];
snprintf(buf, sizeof(buf), "%s", s.c_str());
ImGui::SetNextItemWidth(170.0f);
if (ImGui::InputText("##cond", buf, sizeof(buf),
ImGuiInputTextFlags_EnterReturnsTrue) ||
ImGui::IsItemDeactivatedAfterEdit())
{
t.cond = Engine::Animation::AnimationController::ParseCond(
buf);
}
if (!t.cond.always && !t.cond.valid)
{
ImGui::SameLine();
ImGui::TextColored(WarnCol(), "!");
}
}
ImGui::SameLine();
ImGui::SetNextItemWidth(60.0f);
ImGui::DragFloat("##fade", &t.fade, 0.02f, 0.0f, 5.0f, "%.2f");
ImGui::SameLine();
ImGui::Checkbox("##exit", &t.hasExitTime);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("exit time");
ImGui::Unindent(18.0f);
ImGui::PopID();
}
if (delIdx >= 0)
{
auto& trs = ctrl->TransitionsMut();
trs.erase(trs.begin() + delIdx);
}
if (ImGui::SmallButton("+ Add") && !order.empty())
{
Engine::Animation::Transition tr;
tr.from = order[0];
tr.to = order[order.size() > 1 ? 1 : 0];
ctrl->TransitionsMut().push_back(tr);
}
ImGui::TreePop();
}
// ---- Params:参数声明编辑(禁改名——被 tree/cond 引用,改了断链)----
void ControllerEditorPanel::DrawParams(
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
{
(void)ctx;
if (!ImGui::TreeNode("Params")) return;
auto& ps = ctrl->ParamsMut();
int delIdx = -1;
int pi = 0;
for (auto& p : ps)
{
ImGui::PushID(pi++);
ImGui::TextUnformatted(p.name.c_str());
ImGui::SameLine();
int ty = static_cast<int>(p.type);
ImGui::SetNextItemWidth(80.0f);
if (ImGui::Combo("##ty", &ty, "float\0bool\0trigger\0"))
{
p.type =
static_cast<Engine::Animation::ControllerParam::Type>(ty);
}
ImGui::SameLine();
if (p.type ==
Engine::Animation::ControllerParam::Type::Float)
{
ImGui::SetNextItemWidth(90.0f);
ImGui::DragFloat("##def", &p.defFloat, 0.05f);
}
else if (p.type ==
Engine::Animation::ControllerParam::Type::Bool)
{
ImGui::Checkbox("##def", &p.defBool);
}
ImGui::SameLine();
if (ImGui::SmallButton("x")) delIdx = pi - 1;
ImGui::PopID();
}
if (delIdx >= 0) ps.erase(ps.begin() + delIdx);
{
static char s_NewName[64] = "";
ImGui::SetNextItemWidth(120.0f);
ImGui::InputText("##new_param", s_NewName, sizeof(s_NewName));
ImGui::SameLine();
if (ImGui::SmallButton("+ Add") && s_NewName[0] != '\0')
{
bool dup = false;
for (const auto& p : ps)
{
if (p.name == s_NewName) dup = true;
}
if (!dup)
{
Engine::Animation::ControllerParam d;
d.name = s_NewName;
ps.push_back(d);
s_NewName[0] = '\0';
}
}
}
ImGui::TreePop();
}
+58
View File
@@ -0,0 +1,58 @@
//
// ControllerEditorPanel:动画控制器编辑器窗口(Unity Animator 状态机
// 面板范式,交互对齐 MaterialEditorPanel 的"指派/编辑分离")。
//
// Details 的 Controller 区只做指派(槽/Create/双击打开本面板);
// 结构编辑全在这里:
// - Preview:方向盘(2D 混合树可视化驱动)+ 权重实时读出,拖动
// SetFloat 到当前选中实体(Controller 模式)实时预览
// - States:状态列表(增删/重命名/entry),选中状态详情 = 播放
// 参数 + 混合树类型 + children 逐行编辑(clip 指定/定位/删除)
// - Transitions / Params:列表编辑,cond 字符串 Enter 就地编译
//
// 编辑直接写缓存资产实例(System 下一帧生效,无需保存);Save 落盘
// + ReloadController 重编译(实例换新,本面板每帧重解析不持指针)。
//
#ifndef HELLOVULKAN_CONTROLLEREDITORPANEL_H
#define HELLOVULKAN_CONTROLLEREDITORPANEL_H
#include "EditorContext.h"
#include <Engine/Core/Types.h>
#include <string>
#include <vector>
// .animctrl 拖拽 payload(AssetBrowser → Details Controller 槽指派)
inline constexpr char kControllerDragPayload[] = "CTRL_PATH";
class ControllerEditorPanel {
public:
// 打开 .animctrl 资产(Details 槽位 Edit 按钮 / Content Browser
// 双击入口)
void Open(const std::string& path)
{
m_Path = path;
m_Open = true;
m_SelState = 0;
}
bool IsOpen() const { return m_Open; }
void Draw(const EditorContext& ctx);
private:
void DrawPreview(const EditorContext& ctx);
void DrawStates(const EditorContext& ctx,
Engine::Animation::AnimationController* ctrl);
void DrawTransitions(const EditorContext& ctx,
Engine::Animation::AnimationController* ctrl);
void DrawParams(const EditorContext& ctx,
Engine::Animation::AnimationController* ctrl);
bool m_Open = false;
std::string m_Path;
int m_SelState = 0; // 选中状态(StateOrder 下标)
std::vector<char> m_CondBuf; // cond 输入缓冲(按行复用)
};
#endif //HELLOVULKAN_CONTROLLEREDITORPANEL_H
+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
+361
View File
@@ -0,0 +1,361 @@
//
// HierarchyPanel.cpp:实体层级树实现(UE Outliner 语义)。
//
// 树形展示(根 = 无有效父链接的实体,子 = HierarchyComponent 挂载序)、
// 点击选中、拖拽改父/拖到空白区挂根(keepWorld = 挂载前后世界位姿
// 不变)、右键 Create Child / Detach / Delete(级联随葬,延迟销毁)。
//
#include "HierarchyPanel.h"
#include "AssetBrowserPanel.h" // kPrefabDragPayload(Content Browser 源)
#include "EditorColors.h" // 实体名/组件徽标配色
#include "Engine/ECS/Components.h"
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
#include "Engine/Serialization/SceneSerializer.h"
#include "Engine/Core/Log.h"
#include <imgui.h>
#include <cstring>
#include <filesystem>
#include <string>
#include <vector>
using namespace Engine;
namespace {
// 拖拽 payload 类型名(Editor 内部约定;实体位模式 4 字节)
constexpr const char* kHierPayload = "HIER_ENTITY";
// 实体显示名
std::string DisplayName(const entt::registry& reg, entt::entity e)
{
if (const auto* n = reg.try_get<Engine::ECS::NameComponent>(e);
n && !n->name.empty()) {
return n->name;
}
return "(unnamed)";
}
// Prefab 拖入实例化(树节点/空白区落点共用)。根实体挂在场景根、
// 保留授权位姿;返回根实体(失败 entt::null)
entt::entity InstantiatePrefabDrop(const EditorContext& ctx,
const char* path)
{
if (!ctx.scene || !ctx.assets || !path) {
return entt::null;
}
return Engine::Serialization::SceneSerializer::InstantiatePrefab(
*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<Engine::ECS::MeshRendererComponent>().model = model;
ENGINE_INFO("Hierarchy: model entity '{}' created from '{}'",
name, path);
return static_cast<entt::entity>(e);
}
// 主组件 → 实体名颜色(优先级高者胜,用于 Outliner 可读性)
ImVec4 PrimaryColor(const entt::registry& reg, entt::entity e)
{
using namespace Engine::ECS;
if (reg.all_of<CameraComponent>(e)) return EditorColors::kCamera;
if (reg.all_of<LightComponent>(e)) return EditorColors::kLight;
if (reg.all_of<SkyboxComponent>(e) ||
reg.all_of<SkyLightComponent>(e)) return EditorColors::kSky;
if (reg.all_of<MeshRendererComponent>(e)) return EditorColors::kMesh;
if (reg.all_of<AnimationComponent>(e)) return EditorColors::kAnimB;
if (reg.all_of<ScriptComponent>(e)) return EditorColors::kScript;
if (reg.all_of<RigidBodyComponent>(e) ||
reg.all_of<CharacterControllerComponent>(e))
{
return EditorColors::kPhysics;
}
return EditorColors::kEntityNeutral;
}
// 实体名 → 合法文件名(路径分隔符/保留符净化;空名兜底)
std::string PrefabFileName(std::string name)
{
for (char& c : name) {
if (c == '/' || c == '\\' || c == ':' || c == '*' ||
c == '?' || c == '"' || c == '<' || c == '>' ||
c == '|') {
c = '_';
}
}
if (name.empty() || name == "(unnamed)") {
name = "prefab";
}
return name;
}
// 递归绘制单节点(含子树)。返回值无意义,选中写 selected
void DrawEntityNode(const EditorContext& ctx, entt::entity e,
entt::entity& selected)
{
auto& reg = ctx.scene->GetRegistry();
// 稳定行 ID(UUID):折叠态跨帧保持 + 与 Inspector PushID 隔离
u64 rowId = static_cast<u64>(static_cast<u32>(e));
if (const auto* id = reg.try_get<Engine::ECS::IdComponent>(e)) {
rowId = id->uuid.Value();
}
ImGui::PushID(reinterpret_cast<const void*>(rowId));
// 子表(过滤悬空 + 无 Name 的防御性兜底)
const auto* h = reg.try_get<Engine::ECS::HierarchyComponent>(e);
std::vector<entt::entity> kids;
if (h) {
for (entt::entity c : h->children) {
if (reg.valid(c) && reg.all_of<Engine::ECS::NameComponent>(c)) {
kids.push_back(c);
}
}
}
const std::string label = DisplayName(reg, e);
const ImGuiTreeNodeFlags flags =
ImGuiTreeNodeFlags_OpenOnArrow |
ImGuiTreeNodeFlags_SpanAvailWidth |
(kids.empty() ? ImGuiTreeNodeFlags_Leaf : 0) |
(selected == e ? ImGuiTreeNodeFlags_Selected : 0);
// 实体名按主组件着色(无组件 = 中性色)
ImGui::PushStyleColor(ImGuiCol_Text, PrimaryColor(reg, e));
const bool opened = ImGui::TreeNodeEx(label.c_str(), flags);
ImGui::PopStyleColor();
const bool toggled = ImGui::IsItemToggledOpen();
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && !toggled) {
selected = e;
}
// ---- 拖拽源:拖动本实体去改父 ----
if (ImGui::BeginDragDropSource()) {
const u32 bits = static_cast<u32>(entt::to_integral(e));
ImGui::SetDragDropPayload(kHierPayload, &bits, sizeof(bits));
ImGui::TextUnformatted(label.c_str());
ImGui::EndDragDropSource();
}
// ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变);
// Prefab 拖入 = 实例化子树后挂到本实体下(keepWorld 保持
// 授权世界位姿)----
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kHierPayload)) {
u32 bits = 0;
std::memcpy(&bits, pl->Data, sizeof(bits));
const entt::entity dragged = static_cast<entt::entity>(bits);
// 拒挂自身/自身后代(SetParent 内部同样防环,这里提前
// 静默拦截,避免"看起来挂上了"的错觉)
if (dragged != e && !ctx.scene->IsDescendantOf(dragged, e)) {
ctx.scene->SetParent(dragged, e);
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
if (const entt::entity root =
InstantiatePrefabDrop(ctx,
static_cast<const char*>(
pl->Data));
root != entt::null) {
ctx.scene->SetParent(root, e); // keepWorld 默认 true
selected = root;
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kModelDragPayload)) {
if (const entt::entity me = SpawnModelDrop(
ctx, static_cast<const char*>(pl->Data));
me != entt::null) {
ctx.scene->SetParent(me, e); // keepWorld 默认 true
selected = me;
}
}
ImGui::EndDragDropTarget();
}
// ---- 右键菜单 ----
if (ImGui::BeginPopupContextItem("hier_ctx")) {
if (ImGui::MenuItem("Create Child")) {
// keepWorld=true:新实体出现在父的位置(UE attach 语义)
const auto child = ctx.scene->CreateEntity("child entity");
ctx.scene->SetParent(child, e);
selected = child;
}
if (ImGui::MenuItem("Save As Prefab")) {
// 固定目录 + 实体名派生文件名(无文件对话框库,与场景
// File 菜单同策略;重名覆盖,改名走文件系统)
const std::string path =
"assets/prefabs/" +
PrefabFileName(DisplayName(reg, e)) + ".prefab";
std::error_code ec;
std::filesystem::create_directories("assets/prefabs", ec);
if (Serialization::SceneSerializer::SerializePrefab(
*ctx.scene, *ctx.assets, e, path)) {
ENGINE_INFO("Hierarchy: prefab saved to '{}'", path);
}
}
if (ctx.scene->GetParent(e) != entt::null &&
ImGui::MenuItem("Detach to Root")) {
ctx.scene->SetParent(e, entt::null);
}
if (ImGui::MenuItem("Delete")) {
// 级联随葬:选中若是被删子树的一部分,一并清除
const bool selInSubtree = ctx.scene->IsDescendantOf(selected, e);
ctx.scene->DestroyEntity(e);
if (selInSubtree) {
selected = entt::null;
}
}
ImGui::EndPopup();
}
// ---- 组件徽标(逐组件着色,与行内同级显示)----
bool anyBadge = false;
auto badge = [&](const char* text, const ImVec4& col)
{
ImGui::SameLine(0.0f, 6.0f);
ImGui::PushStyleColor(ImGuiCol_Text, col);
ImGui::TextUnformatted(text);
ImGui::PopStyleColor();
anyBadge = true;
};
if (reg.all_of<Engine::ECS::MeshRendererComponent>(e))
badge("M", EditorColors::kMesh);
if (reg.all_of<Engine::ECS::AnimationComponent>(e))
badge("A", EditorColors::kAnimB);
if (reg.all_of<Engine::ECS::CameraComponent>(e))
badge("C", EditorColors::kCamera);
if (reg.all_of<Engine::ECS::LightComponent>(e))
badge("L", EditorColors::kLight);
if (reg.all_of<Engine::ECS::RigidBodyComponent>(e) ||
reg.all_of<Engine::ECS::CharacterControllerComponent>(e))
badge("P", EditorColors::kPhysics);
if (reg.all_of<Engine::ECS::ScriptComponent>(e))
badge("S", EditorColors::kScript);
if (reg.all_of<Engine::ECS::SkyboxComponent>(e) ||
reg.all_of<Engine::ECS::SkyLightComponent>(e))
badge("K", EditorColors::kSky);
if (anyBadge && ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"M=Mesh A=Animation C=Camera L=Light P=Physics "
"S=Script K=Sky");
}
if (opened) {
for (entt::entity c : kids) {
DrawEntityNode(ctx, c, selected);
}
ImGui::TreePop();
}
ImGui::PopID();
}
} // namespace
void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
{
ImGui::Begin("Outliner");
// 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构)。
// Place Actors 简化语义:常用类型一键带组件,其余 Add Component 补
if (ImGui::Button("+ Create"))
{
ImGui::OpenPopup("create_entity");
}
if (ImGui::BeginPopup("create_entity"))
{
if (ImGui::MenuItem("Empty"))
{
selected = ctx.scene->CreateEntity("Entity");
}
if (ImGui::MenuItem("Light"))
{
auto e = ctx.scene->CreateEntity("Light");
e.AddComponent<Engine::ECS::LightComponent>();
selected = e;
}
if (ImGui::MenuItem("Camera"))
{
auto e = ctx.scene->CreateEntity("Camera");
e.AddComponent<Engine::ECS::CameraComponent>();
selected = e;
}
if (ImGui::MenuItem("Environment"))
{
// UE 新关卡环境集简化:Skybox(背景)+ SkyLight(环境光)一体
auto e = ctx.scene->CreateEntity("Environment");
e.AddComponent<Engine::ECS::SkyboxComponent>();
e.AddComponent<Engine::ECS::SkyLightComponent>();
selected = e;
}
ImGui::EndPopup();
}
ImGui::Separator();
auto& reg = ctx.scene->GetRegistry();
// 树形遍历:根 = 无有效父链接的实体(creation 序),子按挂载序递归
reg.view<Engine::ECS::NameComponent>().each(
[&](const entt::entity e, const Engine::ECS::NameComponent&)
{
if (ctx.scene->GetParent(e) != entt::null) return; // 非根
DrawEntityNode(ctx, e, selected);
});
// ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层);
// Prefab 拖入 = 实例化到场景根(授权位姿)----
ImVec2 rem = ImGui::GetContentRegionAvail();
if (rem.y > 4.0f) {
ImGui::InvisibleButton("##hier_drop_root", rem);
if (ImGui::BeginDragDropTarget()) {
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kHierPayload)) {
u32 bits = 0;
std::memcpy(&bits, pl->Data, sizeof(bits));
ctx.scene->SetParent(static_cast<entt::entity>(bits),
entt::null);
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kPrefabDragPayload)) {
if (const entt::entity root = InstantiatePrefabDrop(
ctx, static_cast<const char*>(pl->Data));
root != entt::null) {
selected = root;
}
}
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kModelDragPayload)) {
if (const entt::entity me = SpawnModelDrop(
ctx, static_cast<const char*>(pl->Data));
me != entt::null) {
selected = me;
}
}
ImGui::EndDragDropTarget();
}
}
ImGui::End();
}
+21
View File
@@ -0,0 +1,21 @@
//
// HierarchyPanel:实体层级树面板。
//
// 树形展示父子层级(UE Outliner)、点击选中、拖拽改父/拖到空白挂根
//(keepWorld)、右键 Create Child / Detach / Delete(级联随葬,帧尾
// flush,遍历安全)。每项尾缀显示组件构成徽标。
//
#ifndef HELLOVULKAN_HIERARCHYPANEL_H
#define HELLOVULKAN_HIERARCHYPANEL_H
#include "EditorContext.h"
#include <entt/entt.hpp>
class HierarchyPanel {
public:
void Draw(const EditorContext& ctx, entt::entity& selected);
};
#endif //HELLOVULKAN_HIERARCHYPANEL_H
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
//
// InspectorPanel:选中实体的组件检查/编辑面板。
//
// 泛化编辑:任何实体(不是沙盒特例)的 Name / Transform /
// MeshRenderer / Light / Camera / Animation 全部在此编辑。
// 沙盒原 Scene Debug 里的 KeyLight、动画 clip、gun scale 控件
// 全部由这里的通用控件替代。
//
#ifndef HELLOVULKAN_INSPECTORPANEL_H
#define HELLOVULKAN_INSPECTORPANEL_H
#include "EditorContext.h"
#include <entt/entt.hpp>
class InspectorPanel {
public:
void Draw(const EditorContext& ctx, entt::entity selected);
};
#endif //HELLOVULKAN_INSPECTORPANEL_H
+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
+499
View File
@@ -0,0 +1,499 @@
//
// MaterialEditorPanel.cpp:独立材质编辑器实现。
//
// 编辑语义原样承接自原 Inspector "Edit Material" 内嵌块(行为不变的
// 搬家):参数 EditParams 直写 UBO + IsItemDeactivatedAfterEdit 落盘;
// 纹理槽改动落盘 + ReloadMaterialAsset 重建(旧指针即刻悬空,本帧
// 不再触碰 asset)。
//
#include "MaterialEditorPanel.h"
#include "MaterialAssetUtil.h"
#include "AssetItemView.h" // 纹理选择器网格(缩略图瓦片)
#include "EditorColors.h"
#include "ThumbnailCache.h"
#include <Engine/ECS/Components.h>
#include <Engine/Renderer/Model.h>
#include <imgui.h>
#include <cstdio>
#include <filesystem>
#include <string>
namespace {
// InputText 需要 char 缓冲;提交时写回 std::string(与 Inspector 同款)
bool InputTextString(const char* label, std::string& str)
{
char buf[256];
std::snprintf(buf, sizeof(buf), "%s", str.c_str());
if (ImGui::InputText(label, buf, sizeof(buf)))
{
str = buf;
return true;
}
return false;
}
constexpr float kPreviewSize = 128.0f; // 编辑器顶部大球(Inspector 槽位为 48²)
// 纹理槽缩略图(同时是拖拽落点):Content Browser 拖纹理资产落入即
// 指派该槽(kTextureDragPayload)。空槽显示 "nil" 占位
void SlotThumb(const std::string& texPath, float sz, int slotIdx,
int& pendingSlot, std::string& pendingPath)
{
const ImVec4 accent = EditorColors::kTexture;
ImGui::Dummy(ImVec2(sz, sz));
const ImVec2 p0 = ImGui::GetItemRectMin();
const ImVec2 p1 = ImGui::GetItemRectMax();
const bool hovered = ImGui::IsItemHovered();
ImDrawList* dl = ImGui::GetWindowDrawList();
bool drawn = false;
if (!texPath.empty())
{
const ThumbView v = EditorThumbnailCache::Get().Request(texPath);
if (v.state == ThumbView::State::Ready)
{
float fw = sz, fh = sz;
if (v.w > 0 && v.h > 0)
{
if (v.w >= v.h) fh = sz * static_cast<float>(v.h) / v.w;
else fw = sz * static_cast<float>(v.w) / v.h;
}
const ImVec2 f0(p0.x + (sz - fw) * 0.5f,
p0.y + (sz - fh) * 0.5f);
dl->AddImage(v.tex, f0, ImVec2(f0.x + fw, f0.y + fh));
drawn = true;
}
}
if (!drawn)
{
// 背板只在未出图时铺:出图时叠加会污染透明像素
dl->AddRectFilled(
p0, p1,
ImGui::GetColorU32(ImVec4(accent.x * 0.25f, accent.y * 0.25f,
accent.z * 0.25f, 1.0f)),
3.0f);
const char* lbl = texPath.empty() ? "nil" : "TEX";
const ImVec2 ts = ImGui::CalcTextSize(lbl);
dl->AddText(ImVec2(p0.x + (sz - ts.x) * 0.5f,
p0.y + (sz - ts.y) * 0.5f),
ImGui::GetColorU32(texPath.empty()
? EditorColors::kOther
: accent),
lbl);
}
dl->AddRect(p0, p1,
ImGui::GetColorU32(hovered ? EditorColors::Lighten(accent,
0.3f)
: accent),
3.0f);
if (ImGui::BeginDragDropTarget())
{
if (const ImGuiPayload* pl =
ImGui::AcceptDragDropPayload(kTextureDragPayload))
{
pendingSlot = slotIdx;
pendingPath = std::string(static_cast<const char*>(pl->Data));
}
ImGui::EndDragDropTarget();
}
}
// 纹理选择器内容(弹窗内):<none> 清除入口 + 缩略图网格。
// 返回 true = 本次有选择,选中路径写入 out(空串 = <none>)
bool TexturePickerContent(const EditorContext& ctx, std::string& out)
{
bool picked = false;
if (ImGui::Selectable("<none>"))
{
out.clear();
picked = true;
}
ImGui::Separator();
const auto& files = EditorMaterialUtil::AssetTextureFiles();
if (files.empty())
{
ImGui::TextDisabled("(no textures under assets/)");
return picked;
}
constexpr float kTile = 64.0f;
const float cellW = kTile + ImGui::GetStyle().CellPadding.x * 2.0f;
int cols = static_cast<int>(360.0f / cellW);
if (cols < 1)
{
cols = 1;
}
ImGui::BeginChild("##texgrid", ImVec2(360.0f, 260.0f),
ImGuiChildFlags_Borders);
if (ImGui::BeginTable("##grid", cols, ImGuiTableFlags_SizingFixedFit))
{
for (int i = 0; i < cols; ++i)
{
ImGui::TableSetupColumn("##c",
ImGuiTableColumnFlags_WidthFixed,
cellW);
}
for (const auto& f : files)
{
ImGui::TableNextColumn();
const auto r =
EditorAssetView::DrawTile(ctx, f, kTile, false);
if (r.clicked)
{
out = f;
picked = true;
}
}
ImGui::EndTable();
}
ImGui::EndChild();
return picked;
}
} // namespace
void MaterialEditorPanel::OpenAsset(const std::string& path)
{
m_AssetMode = true;
m_Path = path;
m_LoadTried = false;
m_Owner = entt::null;
m_Open = true;
}
void MaterialEditorPanel::OpenBuiltin(entt::entity owner, Engine::u32 meshIdx,
const std::string& displayName)
{
m_AssetMode = false;
m_Owner = owner;
m_MeshIdx = meshIdx;
m_DisplayName = displayName;
m_Path.clear();
m_Open = true;
}
void MaterialEditorPanel::Draw(const EditorContext& ctx)
{
if (!m_Open)
{
return;
}
if (!ImGui::Begin("Material Editor", &m_Open))
{
ImGui::End();
return;
}
if (m_AssetMode)
{
DrawAssetMode(ctx);
}
else
{
DrawBuiltinMode(ctx);
}
ImGui::End();
}
// 顶部预览球(tile 池请求幂等——窗口开着才请求,池按可见项换页)
void MaterialEditorPanel::DrawPreview(const EditorContext& ctx,
Engine::Material* mat)
{
Engine::i32 tile = -1;
void* atlasTex = nullptr;
if (ctx.renderer && mat)
{
tile = ctx.renderer->RequestMaterialPreview(mat);
atlasTex = ctx.renderer->GetMaterialPreviewTexture();
}
if (atlasTex && tile >= 0)
{
const float uvStep = 1.0f / float(Engine::Passes::kPreviewTilesPerSide);
const float u0 =
float(tile % Engine::Passes::kPreviewTilesPerSide) * uvStep;
const float v0 =
float(tile / Engine::Passes::kPreviewTilesPerSide) * uvStep;
ImGui::Image(atlasTex, ImVec2(kPreviewSize, kPreviewSize),
ImVec2(u0, v0), ImVec2(u0 + uvStep, v0 + uvStep));
}
else
{
ImGui::Dummy(ImVec2(kPreviewSize, kPreviewSize));
}
}
void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
{
if (!ctx.assets)
{
return;
}
// 每帧重解析:ReloadMaterialAsset 换实例 / 未加载自动 Load 一次
Engine::MaterialAsset* asset = ctx.assets->FindMaterialAsset(m_Path);
if (!asset && !m_LoadTried)
{
m_LoadTried = true;
ctx.assets->LoadMaterialAsset(m_Path);
asset = ctx.assets->FindMaterialAsset(m_Path);
}
if (!asset)
{
ImGui::TextDisabled("material not loaded: %s", m_Path.c_str());
if (ImGui::Button("Retry Load"))
{
m_LoadTried = false;
}
return;
}
const std::string& path = m_Path;
const std::string name =
std::filesystem::path(path).filename().generic_string();
// ---- 头部:预览球 + 资产名/路径 ----
DrawPreview(ctx, asset->GetMaterial());
ImGui::SameLine();
ImGui::BeginGroup();
ImGui::TextUnformatted(name.c_str());
ImGui::TextDisabled("%s", path.c_str());
ImGui::TextDisabled("shared asset: edits apply to all entities "
"referencing it");
ImGui::EndGroup();
ImGui::Separator();
// ---- 透明队列归属(收集侧 CPU 标志)----
bool trans = asset->GetMaterial()->IsTransparent();
if (ImGui::Checkbox("Transparent", &trans))
{
asset->SetTransparent(trans);
Engine::MaterialAsset::Save(path, asset->GetData());
}
// ---- 参数(变化直写 UBO 实时预览;松手落盘,拖拽中不产生每帧 IO)----
Engine::MaterialParams p = asset->GetData().params;
auto EditParam = [&](bool changed)
{
if (changed)
{
asset->EditParams(p);
}
if (ImGui::IsItemDeactivatedAfterEdit())
{
Engine::MaterialAsset::Save(path, asset->GetData());
}
};
EditParam(
ImGui::DragFloat("Metallic", &p.factors.x, 0.01f, 0.0f, 1.0f));
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 槽
// 兜底贴图恒黑,无贴图时颜色因子是唯一入口)
EditParam(ImGui::ColorEdit3("Emissive Color", &p.emissiveFactor.x));
EditParam(ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f,
0.0f, 100.0f));
// transmission/thickness 只有不透明段不读——透明 shader 专属
//(未勾 Transparent 时禁用编辑,防"拖了没反应"的困惑)
ImGui::BeginDisabled(!trans);
EditParam(ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f,
1.0f));
EditParam(ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f,
10.0f));
ImGui::EndDisabled();
if (!trans)
{
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);
}
ImGui::SameLine();
{
// DirectX 法线绿通道翻转(UE 打包 Specular 资产由导入器默认为开;
// 方向反了在此逐材质纠正)。写 misc.w,旧 .material 缺省 0 = 关闭
bool flipG = p.misc.w > 0.5f;
const bool c = ImGui::Checkbox("Flip Green (DirectX)", &flipG);
if (c) { p.misc.w = flipG ? 1.0f : 0.0f; }
EditParam(c);
}
// ---- 纹理槽(变化 → 落盘 + waitIdle 重建)----
ImGui::SeparatorText("Textures");
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
"Albedo", "Normal", "MetallicRoughness",
"Emissive", "Ao", "Roughness"};
int pendingSlot = -1;
std::string pendingPath;
const float thumbSz = ImGui::GetTextLineHeight() * 1.8f;
for (Engine::u32 s = 0; s < Engine::Material::kSlotCount; ++s)
{
std::string tp = asset->GetData().texPaths[s];
ImGui::PushID(static_cast<int>(s));
// 缩略图块(落点:拖入纹理资产即指派该槽)
SlotThumb(tp, thumbSz, static_cast<int>(s), pendingSlot, pendingPath);
ImGui::SameLine();
ImGui::SetNextItemWidth(200.0f);
InputTextString(kSlotNames[s], tp);
if (ImGui::IsItemDeactivatedAfterEdit())
{
pendingSlot = static_cast<int>(s);
pendingPath = tp;
}
ImGui::SameLine();
if (ImGui::SmallButton("..."))
{
ImGui::OpenPopup("tex_pick");
}
if (ImGui::BeginPopup("tex_pick"))
{
// 弹窗内只做选择;pending 统一在循环外落盘(避免 popup 内
// 立即重建导致 asset 指针悬空)
std::string chosen;
if (TexturePickerContent(ctx, chosen))
{
pendingSlot = static_cast<int>(s);
pendingPath = chosen;
}
ImGui::EndPopup();
}
ImGui::PopID();
}
if (pendingSlot >= 0)
{
// 拷出数据段 → 落盘 → 重建资产(旧指针即刻悬空:本帧到此不再
// 触碰 asset,下帧 Find 取新实例)。旧材质已在本帧 Request 进
// 预览池脏表(新 tile 未画满三槽位时每帧重入队)——先 drop 再
// 销毁,否则 Render 段解引用悬空指针
Engine::MaterialAssetData d = asset->GetData();
d.texPaths[static_cast<Engine::u32>(pendingSlot)] = pendingPath;
Engine::MaterialAsset::Save(path, d);
if (ctx.renderer)
{
ctx.renderer->DropMaterialPreview(asset->GetMaterial());
}
ctx.assets->ReloadMaterialAsset(path);
}
}
void MaterialEditorPanel::DrawBuiltinMode(const EditorContext& ctx)
{
// 每帧重解析:实体可销毁(PIE Stop 换血)/ 模型可卸载
if (!ctx.scene || !ctx.assets)
{
ImGui::End();
return;
}
auto& reg = ctx.scene->GetRegistry();
auto* mr = reg.valid(m_Owner)
? reg.try_get<Engine::ECS::MeshRendererComponent>(m_Owner)
: nullptr;
const Engine::Model* model =
(mr && mr->model.Valid()) ? ctx.assets->GetModel(mr->model) : nullptr;
const Engine::MeshMaterialInfo* info =
(model && m_MeshIdx < model->GetMeshMaterialInfos().size())
? &model->GetMeshMaterialInfos()[m_MeshIdx]
: nullptr;
if (!mr || !model || !info)
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"built-in material no longer available");
ImGui::TextDisabled("(entity destroyed or model unloaded)");
return;
}
const std::string meshName =
(!info->name.empty())
? info->name
: "Slot " + std::to_string(m_MeshIdx);
// ---- 头部:预览球 + 名称/来源 ----
DrawPreview(ctx, model->GetMeshes()[m_MeshIdx]->GetMaterial());
ImGui::SameLine();
ImGui::BeginGroup();
ImGui::TextUnformatted(meshName.c_str());
ImGui::TextDisabled("built-in material of %s",
std::filesystem::path(
ctx.assets->Models().Path(mr->model))
.filename()
.generic_string()
.c_str());
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f),
"read-only: built-in materials belong to the model");
ImGui::EndGroup();
ImGui::Separator();
// ---- 一键转可编辑:Create Override → 自动切到资产模式 ----
if (ImGui::Button("Create Override"))
{
const std::string rel = EditorMaterialUtil::CreateMaterialOverride(
ctx.assets->Models().Path(mr->model), *info, m_MeshIdx, *mr,
*ctx.assets);
if (!rel.empty())
{
OpenAsset(rel); // 数据已快照进资产,无缝进入编辑
}
}
ImGui::Separator();
// ---- 只读展示(与资产模式同款控件,锁编辑看值)----
ImGui::BeginDisabled(true);
bool trans = info->transparent;
ImGui::Checkbox("Transparent", &trans);
Engine::MaterialParams p = info->params;
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);
ImGui::BeginDisabled(!trans);
ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f, 1.0f);
ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f, 10.0f);
ImGui::EndDisabled();
if (!trans)
{
ImGui::TextDisabled("Transmission/Thickness need Transparent");
}
ImGui::SeparatorText("Textures");
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
"Albedo", "Normal", "MetallicRoughness",
"Emissive", "Ao", "Roughness"};
for (Engine::u32 s = 0; s < Engine::Material::kSlotCount; ++s)
{
std::string tp = info->texPaths[s];
ImGui::PushID(static_cast<int>(s));
InputTextString(kSlotNames[s], tp);
ImGui::PopID();
}
ImGui::EndDisabled();
}
+63
View File
@@ -0,0 +1,63 @@
//
// MaterialEditorPanel:独立材质编辑器窗口(UE Material Editor 语义)。
//
// 与 Details 面板职责分离:Details 材质槽行只做"指派"(换绑/还原/拖拽
// 覆盖资产),双击槽位预览球或 Content Browser 的 .material 条目 →
// 本面板打开做"编辑"(参数全平铺 + 纹理槽)。
//
// 两种模式:
// - 资产模式(可编辑):编辑 .material 资产。参数走
// EditParams 直写 UBO(松手落盘),纹理槽变化走 ReloadMaterialAsset
// (waitIdle 全停重建,低频路径)——与原 Inspector 内嵌编辑完全
// 同一套语义,此处原样搬运
// - 内建模式(只读):Model 内建材质无 CPU 参数副本(直写 GPU UBO),
// 只读展示 MeshMaterialInfo 快照 + 站内一键 Create Override(生成
// 资产后自动切换到资产模式编辑)
//
// 打开目标每帧重解析(实体可销毁 / 模型可卸载 / 资产可 Reload 换实例),
// 面板不跨帧持有任何指针。
//
#ifndef HELLOVULKAN_MATERIALEDITORPANEL_H
#define HELLOVULKAN_MATERIALEDITORPANEL_H
#include "EditorContext.h"
#include <Engine/Core/Types.h>
#include <entt/entt.hpp>
#include <string>
// 材质资产拖拽 payload(Content Browser 拖 .material → Details 槽位)
inline constexpr const char* kMaterialDragPayload = "MAT_PATH";
// 纹理资产拖拽 payload(Content Browser 拖 .png/.jpg/... → 材质纹理槽)
inline constexpr const char* kTextureDragPayload = "TEXTURE_PATH";
class MaterialEditorPanel {
public:
// 打开 .material 资产(可编辑模式)
void OpenAsset(const std::string& path);
// 打开内建材质(只读模式):owner 实体 + mesh 槽位每帧重解析
void OpenBuiltin(entt::entity owner, Engine::u32 meshIdx,
const std::string& displayName);
void Draw(const EditorContext& ctx);
private:
void DrawPreview(const EditorContext& ctx, Engine::Material* mat);
void DrawAssetMode(const EditorContext& ctx);
void DrawBuiltinMode(const EditorContext& ctx);
bool m_Open = false;
bool m_AssetMode = false;
// 资产模式目标
std::string m_Path;
bool m_LoadTried = false; // 未加载时自动 Load 一次(防每帧重试)
// 内建模式目标
entt::entity m_Owner{entt::null};
Engine::u32 m_MeshIdx = 0;
std::string m_DisplayName;
};
#endif //HELLOVULKAN_MATERIALEDITORPANEL_H
+235
View File
@@ -0,0 +1,235 @@
//
// StatsPanel.cpp:遥测与全局设置实现(原 main.cpp Scene Debug 搬迁)。
// 面板按 View / Scene / Rendering / Physics / Performance 分组——
// 环境贴图/环境光已实体化(Skybox/SkyLight 组件,Details 配置),
// 此处只留场景级 ClearColor。
//
#include "StatsPanel.h"
#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>
using namespace Engine; // f32(Engine/Core/Types.h)
void StatsPanel::Draw(const EditorContext& ctx)
{
ImGui::Begin("Stats");
auto& renderer = *ctx.renderer;
auto& app = *ctx.app;
// ---- View:看哪里(渲染目标)+ 相机位姿(跟随渲染源分派)----
if (ImGui::CollapsingHeader("View", ImGuiTreeNodeFlags_DefaultOpen))
{
// 渲染源分派与 SceneRenderer::BuildFrame 同规则:PIE 内嵌播放
// 或 kDisplayDirect 全屏预览 = primary 相机,否则编辑器相机
const bool playing = ctx.playState && *ctx.playState;
static const char* displayTarget[] = {
"Fullscreen",
"Viewport",
};
int mode = renderer.GetDisplayMode();
if (ImGui::Combo("Display Target", &mode, displayTarget,
IM_ARRAYSIZE(displayTarget)))
{
renderer.SetDisplayMode(mode);
}
if ((playing || renderer.GetDisplayMode() == kDisplayDirect))
{
Engine::ECS::TransformComponent* camT = nullptr;
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr)
{
// world 位姿(相机实体可能挂父层级下)
const glm::vec3& pos = camT->GetWorldPosition();
ImGui::Text("Game Camera Pos");
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
}
else
{
ImGui::TextDisabled("(no primary camera)");
}
}
else if (ctx.editorCamera)
{
const glm::vec3& pos = ctx.editorCamera->GetTransform().GetPosition();
ImGui::Text("Editor Camera Pos");
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
}
else
{
ImGui::TextDisabled("(no camera)");
}
}
// ---- Scene:模拟控制 + 场景级设置。PIE 暂停在主工具栏(UE 语义)----
if (ImGui::CollapsingHeader("Scene", ImGuiTreeNodeFlags_DefaultOpen))
{
ImGui::ColorEdit4("Clear Color",
glm::value_ptr(ctx.scene->GetSettings().ClearColor));
}
// ---- Rendering:渲染管线参数与开关 ----
if (ImGui::CollapsingHeader("Rendering", ImGuiTreeNodeFlags_DefaultOpen))
{
f32 exposure = renderer.GetExposure();
if (ImGui::SliderFloat("Exposure", &exposure, 0.1f, 4.0f))
{
renderer.SetExposure(exposure);
}
bool aces = renderer.GetUseACES();
if (ImGui::Checkbox("ACES", &aces))
{
renderer.SetUseACES(aces);
}
// Bloom:开关即显隐——勾上才显示参数
bool bloom = renderer.GetBloomEnabled();
if (ImGui::Checkbox("Bloom", &bloom))
{
renderer.SetBloomEnabled(bloom);
}
if (bloom)
{
f32 threshold = renderer.GetBloomThreshold();
if (ImGui::SliderFloat("Threshold", &threshold, 0.0f, 8.0f,
"%.1f", ImGuiSliderFlags_Logarithmic))
{
renderer.SetBloomThreshold(threshold);
}
f32 strength = renderer.GetBloomStrength();
if (ImGui::SliderFloat("Strength", &strength, 0.0f, 2.0f))
{
renderer.SetBloomStrength(strength);
}
}
// 无参数的纯开关:横排平铺
bool shadows = renderer.GetShadowsEnabled();
if (ImGui::Checkbox("Shadows", &shadows))
{
renderer.SetShadowsEnabled(shadows);
}
ImGui::SameLine();
bool taa = renderer.GetTAAEnabled();
if (ImGui::Checkbox("TAA", &taa))
{
renderer.SetTAAEnabled(taa);
}
ImGui::SameLine();
bool lod = renderer.GetLodEnabled();
if (ImGui::Checkbox("LOD", &lod))
{
renderer.SetLodEnabled(lod); // 关 = 恒绘原网格
}
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
const auto& viewNames = renderer.GetDebugViewNames();
int view = renderer.GetDebugView();
if (ImGui::Combo("Debug View", &view, viewNames.data(),
static_cast<int>(viewNames.size())))
{
renderer.SetDebugView(view);
}
}
// ---- Physics:调试可视化(collider/约束/重力/速度/加速度)----
if (ImGui::CollapsingHeader("Physics"))
{
auto& physics = app.GetPhysics();
u32 bits = physics.GetDebugDrawBits();
bool master = bits != 0;
if (ImGui::Checkbox("Debug Draw##Phys", &master))
{
bits = master ? kPhyDbgDefault : 0;
}
if (master)
{
ImGui::Indent();
auto toggle = [&bits](const char* label, u32 bit) {
bool on = (bits & bit) != 0;
if (ImGui::Checkbox(label, &on))
{
bits = on ? (bits | bit) : (bits & ~bit);
}
};
toggle("Colliders", kPhyDbgColliders);
toggle("Constraints", kPhyDbgConstraints);
toggle("Gravity", kPhyDbgGravity);
toggle("Velocity", kPhyDbgVelocity);
toggle("Acceleration", kPhyDbgAccel);
ImGui::Unindent();
}
physics.SetDebugDrawBits(bits);
// 碰撞事件计数(最近一次步进;掉落物触地时应 > 0)
ImGui::Text("contacts (frame): %zu", physics.GetEvents().size());
}
// ---- Performance:帧率控制 + GPU 遥测(默认折叠,不占面板高度)----
if (ImGui::CollapsingHeader("Performance"))
{
bool vsync = app.GetVSync();
if (ImGui::Checkbox("VSync", &vsync))
{
app.SetVSync(vsync);
if (!vsync)
{
const f32 target = app.GetTargetFps();
app.SetTargetFps(target);
}
}
if (!vsync)
{
f32 target = app.GetTargetFps();
if (ImGui::SliderFloat("Target FPS", &target, 24.0f, 360.0f, "%.0f"))
{
app.SetTargetFps(target);
}
}
ImGui::Text("fps: %.1f frame: %llu",
1.0f / Time::Current().UnscaledDt(),
static_cast<unsigned long long>(app.GetFrameCount()));
ImGui::Separator();
ImGui::Text("instances: %u main / %u shadow",
renderer.GetMainInstanceCount(),
renderer.GetShadowInstanceCount());
if (renderer.GetInstancesTruncated())
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"TRUNCATED: instance buffer full!");
}
const auto& names = renderer.GetPassNames();
const auto& times = renderer.GetPassGpuMs();
for (size_t i = 0; i < names.size() && i < times.size(); ++i)
{
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();
}
+19
View File
@@ -0,0 +1,19 @@
//
// StatsPanel:遥测与全局设置面板(原 Scene Debug 拆入)。
//
// Camera 位姿 / Rendering 开关 / Environment(IBL+DebugView+ClearColor) /
// GPU Timings / Frame(VSync、fps)。KeyLight、动画 clip、gun scale
// 等沙盒特例编辑已泛化进 Inspector,此处不再重复。
//
#ifndef HELLOVULKAN_STATSPANEL_H
#define HELLOVULKAN_STATSPANEL_H
#include "EditorContext.h"
class StatsPanel {
public:
void Draw(const EditorContext& ctx);
};
#endif //HELLOVULKAN_STATSPANEL_H
+484
View File
@@ -0,0 +1,484 @@
//
// ViewportPanel:场景渲染输出面板。
//
// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供
// Editor Viewport / Fullscreen 显示模式切换。面板可用区域
// (物理像素)上报为渲染分辨率 → 画面精确填满面板。
//
// 点击拾取(UE 式 GPU HitProxy):左键单击 → 鼠标 NDC 经
// Renderer::RequestPick 请求下一帧 id 渲染 + 像素拷贝;Draw 每帧
// 轮询 TakePickResult 取走结果。像素级精度,与视觉画面一致
//(视觉几何语义:与 Collider 无关,无 collider 实体同样可选中)。
//
#include "ViewportPanel.h"
#include <imgui.h>
#include <ImGuizmo.h>
#include <Engine/ECS/Components.h> // MeshRenderer / Name / RigidBody(日志对照)
#include <Engine/Renderer/Renderer.h> // RequestPick / TakePickResult 门面
#include <Engine/Renderer/SceneTypes.h> // kPickMiss
#include <Engine/Core/Log.h> // 命中打印
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <cstdio>
// GTX 扩展(实验性)需显式开启:matrix_decompose(矩阵→SRT 分解,
// 旋转以四元数输出,直接写回 TransformComponent)
#define GLM_ENABLE_EXPERIMENTAL
#include <glm/gtx/matrix_decompose.hpp>
using namespace Engine; // u32 / f32(Engine/Core/Types.h)
namespace
{
// Ctrl 吸附步长(UE Grid Snap 语义:按住 Ctrl 拖拽才吸附)
constexpr float kSnapTranslate = 0.25f; // 米/格
constexpr float kSnapRotateDeg = 15.0f; // 度
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]):
// Y 行翻回(抵消 Vulkan clip Y 向下翻转);z 行 = 2*z - w 即
// 深度 [0,1] → [-1,1]。glm 列主序 m[col][row],逐列改 row1/row2
glm::mat4 ProjVulkanToGL(const glm::mat4& projVulkan)
{
glm::mat4 p = projVulkan;
for (int c = 0; c < 4; ++c) {
p[c][1] *= -1.0f;
p[c][2] = 2.0f * p[c][2] - p[c][3];
}
return p;
}
// 视图相机分派(必须与 SceneRenderer::BuildFrame 渲染源同一规则,
// 否则 Gizmo 杆与画面错位):编辑器形态 = EditorCamera(Scene view
// 专用,与相机实体无关);游戏形态(kDisplayDirect 全屏预览或 PIE
// 内嵌播放)= primary 相机实体。
// 返回 nullptr = 游戏形态且无 primary 相机(本帧无 Gizmo)
Engine::Camera* ViewCamera(const EditorContext& ctx,
ECS::TransformComponent*& outT)
{
const bool playing = ctx.playState && *ctx.playState;
if (!playing && 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,
ViewportInputState &out,
GizmoState &gizmo,
const entt::entity selected) const {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
std::optional<entt::entity> picked;
{
ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoCollapse);
// 输入焦点上报(悬停计入遮挡:模态框盖住时不误报)
out.hovered = ImGui::IsWindowHovered();
out.focused = ImGui::IsWindowFocused();
auto &renderer = *ctx.renderer;
const int mode = renderer.GetDisplayMode();
// ---- 拾取结果轮询(先于新请求:先取走上帧结果再发新请求)。
// 拷贝随上一帧命令缓冲提交,TakePickResult 内 waitIdle 保证
// 暂存可见(仅拷贝就绪帧阻塞,点击低频不可感知)----
if (auto bits = renderer.TakePickResult()) {
auto &reg = ctx.scene->GetRegistry();
const entt::entity hit = static_cast<entt::entity>(*bits);
if (*bits != kPickMiss && reg.valid(hit)) {
picked = hit;
LogPick(ctx, hit);
} else {
// 空白(kPickMiss)或悬空位模式:清选中(与旧语义一致)
picked = entt::null;
if (*bits == kPickMiss) {
ENGINE_INFO("[pick] miss (sky/empty)");
} else {
ENGINE_WARN("[pick] dangling entity bits {:#x}", *bits);
}
}
}
const void *sceneTex = renderer.GetSceneTexture();
if (sceneTex != nullptr && mode == kDisplayViewport) {
// 面板可用区域(物理像素)上报为渲染分辨率 → 画面精确填满
// 面板;下一帧帧首 BeginFrameSync 生效(一帧延迟,不可见)
const ImVec2 avail = ImGui::GetContentRegionAvail();
const ImVec2 fb = ImGui::GetIO().DisplayFramebufferScale;
const u32 wantW = static_cast<u32>(avail.x * fb.x + 0.5f);
const u32 wantH = static_cast<u32>(avail.y * fb.y + 0.5f);
if (wantW >= 16 && wantH >= 16) {
renderer.SetViewportSize(wantW, wantH);
}
// 纹理 = 渲染分辨率:等比适配面板可用区域
const vk::Extent2D ext = renderer.GetRenderExtent();
const f32 aspect =
ext.height != 0 ? f32(ext.width) / f32(ext.height) : 1.0f;
ImVec2 size = avail;
if (avail.x / avail.y > aspect) size.x = avail.y * aspect;
else size.y = avail.x / aspect;
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
// ---- 骨骼关节 overlay(Endfield-Poser 范式):3D 骨架线
// 给结构,屏幕圆点给可读性。世界→屏幕与渲染同相机,
// Vulkan NDC y 向下与 ImGui 屏幕坐标同构 → 线性映射
// image 矩形即可。hover 关节 = 白圈高亮 + 骨骼名
{
const auto &frameData = renderer.GetFrameSceneData();
if (frameData.showBoneWires && !frameData.pieActive &&
!frameData.boneJoints.empty() &&
ctx.editorCamera != nullptr) {
const ImVec2 rmin = ImGui::GetItemRectMin();
const ImVec2 rmax = ImGui::GetItemRectMax();
const glm::mat4 vp =
ctx.editorCamera->GetController().GetProjection(aspect) *
ctx.editorCamera->GetView();
ImDrawList *dl = ImGui::GetWindowDrawList();
dl->PushClipRect(rmin, rmax, true);
const f32 r = frameData.boneJointPx;
const ImGuiIO &io = ImGui::GetIO();
const ImVec2 mouse = io.MousePos;
int hovered = -1;
float hoveredD = 0.0f;
for (size_t ji = 0; ji < frameData.boneJoints.size(); ++ji) {
const auto &jm = frameData.boneJoints[ji];
const glm::vec4 c = vp * glm::vec4(jm.pos, 1.0f);
if (c.w <= 0.0f) continue; // 相机身后
const ImVec2 p(
rmin.x + (c.x / c.w * 0.5f + 0.5f) * size.x,
rmin.y + (c.y / c.w * 0.5f + 0.5f) * size.y);
// 骨骼 = 蓝(Endfield humanoid 色),普通节点 = 青
const ImU32 fill = jm.isBone
? IM_COL32(70, 160, 255, 230)
: IM_COL32(70, 190, 190, 190);
dl->AddCircleFilled(p, r, fill);
dl->AddCircle(p, r + 1.5f,
IM_COL32(255, 255, 255, 140), 0, 1.2f);
// hover 命中(半径 +3px 容差,取最近)
const float dx = p.x - mouse.x, dy = p.y - mouse.y;
const float d = dx * dx + dy * dy;
const float hit = (r + 3.0f) * (r + 3.0f);
if (d <= hit && (hovered < 0 || d < hoveredD)) {
hovered = static_cast<int>(ji);
hoveredD = d;
}
}
// hover 高亮圈 + 骨骼名 tooltip(名字查选中实体模型
// 节点表;查不到 = 显示节点下标)
if (hovered >= 0 && ImGui::IsItemHovered()) {
const auto &jm = frameData.boneJoints[hovered];
const glm::vec4 c = vp * glm::vec4(jm.pos, 1.0f);
const ImVec2 p(
rmin.x + (c.x / c.w * 0.5f + 0.5f) * size.x,
rmin.y + (c.y / c.w * 0.5f + 0.5f) * size.y);
dl->AddCircle(p, r + 3.5f,
IM_COL32(255, 255, 255, 255), 0, 2.0f);
const char *name = nullptr;
if (selected != entt::null) {
// 本段在 reg 声明之前,就地取 registry
const auto *mrSel =
ctx.scene->GetRegistry().try_get<
Engine::ECS::MeshRendererComponent>(
selected);
if (mrSel && mrSel->model.Valid()) {
const Model *mdl =
ctx.assets->GetModel(mrSel->model);
if (mdl && jm.nodeIdx < mdl->GetNodes().size())
name = mdl->GetNodes()[jm.nodeIdx]
.name.c_str();
}
}
ImGui::SetTooltip(
"%s%s", name ? name : "(node)",
jm.isBone ? "" : " [non-bone]");
}
dl->PopClipRect();
}
}
// ---- 工具热键(UE 语义):Q 纯选择(不出杆)、W/E/R 切
// Gizmo 模式、T 切 local/world、F 焦点选中实体。仅 Viewport
// 持焦时生效——输入框打字(WantTextInput)由 BlockingKeyboard
// 拦游戏侧,但 ImGui 自身仍收键,故必须限定持焦面板。RMB 按
// 住 = 相机飞行模式(键盘归相机),工具热键全部抑制——W 前
// 进与 W 平移 Gizmo 的键位冲突就此解耦 ----
auto &reg = ctx.scene->GetRegistry();
ECS::TransformComponent *tr =
selected != entt::null
? reg.try_get<ECS::TransformComponent>(selected)
: nullptr;
// 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 无任何轴位,
// ImGuizmo 不绘制不响应——选中实体只高亮不出杆
if (ImGui::IsKeyPressed(ImGuiKey_Q)) gizmo.op = 0;
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;
const bool fCombo = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
// F = 焦点选中实体(frame-selection):编辑器相机
// 环绕 pivot 设为选中实体位置,沿视线平移对准(环
// 绕距离沿用当前值,滚轮可调)。世界位姿(层级
// 实体 = 父链组合结果)。裸 F 才触发(组合键排他)
if (ImGui::IsKeyPressed(ImGuiKey_F) && !fCombo && tr != nullptr) {
ctx.editorCamera->FocusOn(tr->GetWorldPosition());
}
}
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
// 选中实体 + 有 Transform 才出杆;分解 world 矩阵经
// SetWorldPose 写回(与 Inspector 同源,下一帧数值自动跟随)
// PIE 运行态不出杆(编辑操作冻结)
if (tr != nullptr && !playing) {
// 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏
// 形态 = primary 相机实体;与 SceneRenderer::BuildFrame
// 同一分派),投影参数由相机自身承载(与渲染侧一致)
ECS::TransformComponent *camT = nullptr;
if (Engine::Camera *cam = ViewCamera(ctx, camT);
cam != nullptr && camT != nullptr) {
const glm::mat4 view = cam->GetView(*camT);
const glm::mat4 projGL = ProjVulkanToGL(
cam->GetProjection(aspect));
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
// 杆画在 Image 区域内(等比适配后的 item rect)
const ImVec2 mn = ImGui::GetItemRectMin();
ImGuizmo::SetRect(mn.x, mn.y, size.x, size.y);
// 不调 SetPlaneLimit/SetAxisLimit——可见度保持库默认
glm::mat4 m = tr->World();
// 操作空间:local(模型轴,旋转/缩放随物体朝向直观)/
// world(T 键切换)。无父层级时两种模式 world 矩阵同源,
// decompose 写回链路不变
const ImGuizmo::MODE mode =
gizmo.localSpace ? ImGuizmo::LOCAL : ImGuizmo::WORLD;
const glm::mat3 rotM(m);
// 退化轴裁剪(防误拖动):被裁轴不可见、不可激活,
// 转个视角即可用。三操作病态几何不同,按 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]
// 逐轴;旋转 = snap[0] 按度(库内自行 DEG2RAD,传弧
// 度会被解释成极小步进);缩放 = 只取 snap[0]
float snap[3] = {0.0f, 0.0f, 0.0f};
const bool snapping = ImGui::GetIO().KeyCtrl;
if (snapping) {
if (gizmo.op == ImGuizmo::TRANSLATE) {
snap[0] = snap[1] = snap[2] = kSnapTranslate;
} else if (gizmo.op == ImGuizmo::ROTATE) {
snap[0] = kSnapRotateDeg; // 度,非弧度
} else if (gizmo.op == ImGuizmo::SCALE) {
snap[0] = kSnapScale;
}
}
ImGuizmo::Manipulate(&view[0][0], &projGL[0][0],
static_cast<ImGuizmo::OPERATION>(gizmo.op),
mode, &m[0][0], nullptr,
snapping ? snap : nullptr);
gizmo.dragging = ImGuizmo::IsUsing();
// 拖拽生效:分解 world 矩阵 → world SRT → 沿父链反解
// local 写回(SetWorldPose;根实体 = 直写)。旋转用
// decompose 出的 quat 直写(UE FTransform 语义)——
// 不经欧拉往返,拖拽零漂移、无万向锁
if (ImGuizmo::IsUsing()) {
glm::vec3 scale, transl, skew;
glm::quat rot;
glm::vec4 persp;
if (glm::decompose(m, scale, rot, transl, skew, persp)) {
ctx.scene->SetWorldPose(selected, transl, rot,
scale);
}
}
}
}
// Gizmo 状态提示(左上角 drawlist 叠加,原因同下:不占布局行)
// PIE 运行态 = 黄色 PLAYING/PAUSED 提示替代编辑状态
{
char tip[128];
if (playing) {
const bool paused = ctx.pausedState && *ctx.pausedState;
std::snprintf(tip, sizeof(tip),
paused ? "PAUSED - Ctrl+P to stop"
: "PLAYING - Ctrl+P to stop");
} else {
std::snprintf(tip, sizeof(tip),
"Q/W/E/R: Sel/Move/Rot/Scale T: %s "
"Snap: Ctrl F: Focus",
gizmo.localSpace ? "Local" : "World");
}
const ImVec2 imgMin = ImGui::GetItemRectMin();
ImGui::GetWindowDrawList()->AddText(
ImVec2(imgMin.x + 8.0f, imgMin.y + 8.0f),
playing ? IM_COL32(255, 184, 46, 220)
: IM_COL32(255, 255, 255, 150),
tip);
}
// Dynamic 刚体提示:物理每帧把姿态写回 Transform,拖拽即刻
// 被覆盖(Static/Kinematic 编辑器权威,可直接拖)。
// 用 drawlist 叠加而非 ImGui::TextDisabled:后者在 Image 下方
// 占一行布局 → 滚动条出现/消失循环 → avail 抖动 → 渲染分辨率
// 每帧重报(视口疯狂顶动);且默认字体无中文字形(显示 ?)
if (playing) {
// 运行态无 primary 相机实体:渲染回退编辑器视角(保持帧
// 管线节拍),提示补相机
ECS::TransformComponent *camT = nullptr;
if (ViewCamera(ctx, camT) == nullptr) {
const ImVec2 imgMin = ImGui::GetItemRectMin();
ImGui::GetWindowDrawList()->AddText(
ImVec2(imgMin.x + 8.0f,
imgMin.y + size.y * 0.5f),
IM_COL32(255, 120, 80, 230),
"No primary camera entity - showing editor "
"view. Add a Camera component and check "
"'Primary'");
}
}
// Dynamic 刚体提示(drawlist 叠加,理由同上)
if (selected != entt::null) {
if (const auto *rb = reg.try_get<ECS::RigidBodyComponent>(selected);
rb != nullptr &&
rb->type == ECS::RigidBodyComponent::BodyType::Dynamic) {
const ImVec2 imgMin = ImGui::GetItemRectMin();
ImGui::GetWindowDrawList()->AddText(
ImVec2(imgMin.x + 8.0f, imgMin.y + size.y - 20.0f),
IM_COL32(255, 255, 255, 160),
"(Dynamic body: transform driven by physics)");
}
}
// 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。
// 抓杆时不出 pick(IsOver 悬停杆/IsUsing 拖拽中)——否则按在
// 操纵杆上的点击会穿透发拾取请求。PIE 运行态不出拾取:
// 点击 = 游戏输入(UE Play 语义,编辑操作冻结)
if (!playing && ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) &&
!ImGuizmo::IsOver() && !ImGuizmo::IsUsing()) {
RequestPickAt(ctx, size);
}
} else {
ImGui::TextDisabled("(scene texture not ready)");
}
ImGui::End();
}
ImGui::PopStyleVar();
return picked;
}
void ViewportPanel::RequestPickAt(const EditorContext &ctx,
const ImVec2 &size) const {
// 鼠标在 Image 内的位置 → NDC。Vulkan NDC y 与屏幕 v 同向
//(向下为正,与渲染同构;skybox.frag 反投影交叉验证过方向)
const ImVec2 mouse = ImGui::GetMousePos();
const ImVec2 mn = ImGui::GetItemRectMin();
const glm::vec2 ndc{
(mouse.x - mn.x) / size.x * 2.0f - 1.0f,
(mouse.y - mn.y) / size.y * 2.0f - 1.0f,
};
// UE 式 GPU HitProxy:下一帧 id 渲染 + 请求像素 1×1 拷贝
ctx.renderer->RequestPick(ndc);
}
void ViewportPanel::LogPick(const EditorContext &ctx, entt::entity e) {
// 命中详情:实体名;有物理体则附 collider 对照(诊断"物理盒 ≠
// 视觉"的错位,如 M1911 的默认 0.5 半尺寸盒)
auto &reg = ctx.scene->GetRegistry();
const auto &name = reg.get<ECS::NameComponent>(e).name;
if (const auto *rb = reg.try_get<ECS::RigidBodyComponent>(e)) {
const auto &tr = reg.get<ECS::TransformComponent>(e);
ENGINE_INFO("[pick] '{}' (hitproxy) | collider={} type={} "
"boxHalf=({:.2f},{:.2f},{:.2f}) sphereR={:.2f} "
"bodyPos=({:.2f},{:.2f},{:.2f})",
name,
rb->collider == ECS::RigidBodyComponent::ColliderType::Box
? "box" : "sphere",
rb->type == ECS::RigidBodyComponent::BodyType::Static
? "static" : "dynamic",
rb->boxHalfExtents.x, rb->boxHalfExtents.y,
rb->boxHalfExtents.z, rb->sphereRadius,
tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z);
} else {
ENGINE_INFO("[pick] '{}' (hitproxy) | (no collider)", name);
}
}
+59
View File
@@ -0,0 +1,59 @@
//
// ViewportPanel:场景渲染输出面板。
//
// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供
// Editor Viewport / Fullscreen 显示模式切换。面板可用区域
// (物理像素)上报为渲染分辨率 → 画面精确填满面板。
//
// 点击拾取(UE 式 GPU HitProxy):单击 → NDC 请求 id 渲染;结果
// 由 Draw 每帧轮询取走(像素级,与视觉画面一致)。
//
#ifndef HELLOVULKAN_VIEWPORTPANEL_H
#define HELLOVULKAN_VIEWPORTPANEL_H
#include "EditorContext.h"
#include <imgui.h> // ImVec2(RequestPickAt 签名)
#include <entt/entt.hpp>
#include <optional>
// Viewport 面板每帧上报的输入焦点状态(Hazel 模式:编辑器据此决定
// 游戏侧输入是否放行——悬停/持焦 Viewport 才拥有 WASD/鼠标)
struct ViewportInputState {
bool hovered = false; // 鼠标悬停在 Viewport 面板上
bool focused = false; // Viewport 面板持有键盘焦点
};
// Gizmo 交互状态(Editor 持有、ViewportPanel 写):
// op 取 ImGuizmo::OPERATION 位掩码(TRANSLATE=7=三轴位或运算,非 0
// 起始枚举;0 = 无任何轴位 → Gizmo 完全不绘制),int 存储避免本
// 头文件 include ImGuizmo.h(拖入 imgui 宏依赖顺序)
// Q 纯选择(op=0 不出杆)、W/E/R 切换 op、T 切换 local/world 空间
// (UE 语义);dragging 拉起 Editor::BlockingMouse 门控
struct GizmoState {
int op = 7; // ImGuizmo::TRANSLATE(TRANSLATE_X|Y|Z)
bool localSpace = true; // 操纵空间:true=模型轴(旋转/缩放语义),
// T 键切 world
bool dragging = false; // 拖拽中(IsUsing)→ 鼠标归 Gizmo 不给游戏侧
};
class ViewportPanel {
public:
// 返回值:本帧拾取命中的实体(无点击/未命中 = nullopt 或 null);
// Editor 据此 SelectEntity(面板不持有选中状态)
std::optional<entt::entity> Draw(const EditorContext& ctx,
ViewportInputState& out,
GizmoState& gizmo,
entt::entity selected) const;
private:
// Image 区域内的鼠标位置 → NDC → Renderer::RequestPick
//(结果下一帧由 Draw 轮询 TakePickResult 取走)
void RequestPickAt(const EditorContext& ctx, const ImVec2& size) const;
// 命中打印:实体名 + collider 参数对照(诊断物理盒 ≠ 视觉用)
static void LogPick(const EditorContext& ctx, entt::entity e);
};
#endif //HELLOVULKAN_VIEWPORTPANEL_H
+147 -112
View File
@@ -1,36 +1,53 @@
//
// Created by atdunbg on 2026/9/20.
//
// SandboxApp:第一个基于 Application 框架的程序。
// SandboxApp:基于 Application 框架的编辑器宿主程序。
//
// main 的全部职责:定义一个 Application 派生类并运行它。
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理;
// 这里只写"程序逻辑":创建实体、逐帧更新组件、事件响应。
// main 的职责边界:
// - 定义 Application 派生类并运行它
// - 编辑器形态驱动 EditorCamera(Scene view 专用相机,非场景对象)
// - 异步环境加载、全局热键、输入归属门控
// - UI 全部委托 App/editor/ 的 docking Editor(本文件不再出现
// 任何 ImGui 控件)
//
// 场景内容(实体/组件)由编辑器 File 菜单 Save/Load 场景文件管理,
// 相机实体属于场景数据、只在游戏形态参与渲染(UE 语义)。
//
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理。
//
#include <Engine/Core/Application.h>
#include <Engine/Core/Time.h>
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
#include <Engine/Core/Log.h>
#include <Engine/Core/Stats.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/EditorCamera.h>
#include <Engine/Serialization/SceneSerializer.h>
#include <imgui.h>
#include <Engine/Core/Input.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <filesystem>
#include <GLFW/glfw3.h>
// editor/game 编译期分离(HELLOVULKAN_EDITOR CMake option):editor 形态
// 挂接 docking Editor;game 形态 = Direct 直渲 + primary 相机,零编辑器代码
#ifdef HELLO_EDITOR
#include "Editor.h"
#include "EditorContext.h"
#endif
using namespace Engine;
using namespace Engine::ECS;
namespace
{
class SandboxApp final : public Application
{
public:
SandboxApp()
: Application("HelloVulkan", 800, 600)
: Application("HelloVulkan", 1920, 1080)
{
}
~SandboxApp() override
{
}
@@ -39,139 +56,157 @@ protected:
void OnInit() override
{
Scene& scene = GetScene();
Renderer& renderer = GetRenderer();
#ifdef HELLO_EDITOR
renderer.SetDisplayMode(kDisplayViewport);
// ImGui 叠加层(框架只做管线桥接,UI 内容全部在这里定义)
// ImGui 叠加层(框架只做管线桥接;UI 内容 = docking Editor)
EnableImGui();
// 模型资产(AssetManager 负责加载/缓存/生命周期,返回句柄)
auto backpack = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
// 地面:程序化纯色平面(半边长 15,中心 y=-2)
auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
m_Ground = scene.CreateEntity("Ground");
m_Ground.AddComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f))
.Compose();
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
// 相机实体(primary = 当前渲染视角)
m_Camera = scene.CreateEntity("MainCamera");
auto& camera = m_Camera.AddComponent<CameraComponent>();
camera.primary = true;
camera.camera.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f));
// 背包实体 × 3(悬浮在地面上方)
for (int i = 0; i < 3; ++i) {
auto e = scene.CreateEntity("Backpack" + std::to_string(i));
e.AddComponent<TransformComponent>()
.SetPosition(glm::vec3((i - 1) * 1.5f, 0.0f, 0.0f))
.SetScale(glm::vec3(0.5f))
.Compose();
e.AddComponent<MeshRendererComponent>(MeshRendererComponent{backpack});
m_Spinners.push_back(e);
// Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
InitEditor();
#else
// game 形态:全窗口 Direct 直渲(Renderer 默认即 kDisplayDirect),
// 相机由场景 primary camera 提供,无 UI。
// 游戏流:Stopped → Loading(OnUpdate 泵场景加载)→ Running
GetGameFlow().RequestStart();
#endif
}
// 灯光实体(第一个灯 = 阴影光源)
m_KeyLight = scene.CreateEntity("KeyLight");
m_KeyLight.AddComponent<LightComponent>(
LightComponent{glm::vec3(-0.5f, -0.6f, -0.35f),
glm::vec4(1.0f, 0.95f, 0.9f, 1.0f)});
scene.CreateEntity("FillLight")
.AddComponent<LightComponent>(
LightComponent{glm::vec3(0.5f, -0.5f, 0.3f),
glm::vec4(0.3f, 0.5f, 1.0f, 0.8f)});
}
void OnUpdate(f32 deltaTime) override
void OnUpdate(const FrameTime& frameTime) override
{
m_Time += deltaTime;
// 相机(WASD/Space/Shift 移动)
m_Camera.GetComponent<CameraComponent>()
.camera.Update(GetWindow(), deltaTime);
// 旋转动画:编辑 Transform 分量后 Compose 重算 world
for (size_t i = 0; i < m_Spinners.size(); ++i) {
auto& t = m_Spinners[i].GetComponent<TransformComponent>();
t.position = glm::vec3((static_cast<f32>(i) - 1.0f) * 1.5f, 0.0f, 0.0f);
t.rotation.y = m_Time * 60.0f + static_cast<f32>(i) * 40.0f;
t.scale = glm::vec3(0.5f);
t.Compose();
#ifdef HELLO_EDITOR
// 编辑器形态:驱动编辑器相机(RMB 飞行 / MMB 焦点环绕 /
// 滚轮推拉)。暂停(timeScale=0)时仍可飞行——unscaled dt。
// PIE 内嵌播放:渲染源已切 primary 相机,编辑器相机停驱
//(否则与游戏脚本抢 WASD,Stop 后视角也被偷跑)。
// 游戏形态:相机实体由游戏逻辑驱动(本沙盒无逻辑 = 静止)
if (GetRenderer().GetDisplayMode() == kDisplayViewport &&
!m_Editor->IsPlaying())
{
GetRenderer().GetEditorCamera().Update(frameTime.UnscaledDt());
}
#else
// Loading 泵(同步示例):场景文件存在则加载,不存在 =
// 空场景照常进 Running(引擎冒烟路径)。加载完成后必须
// CompleteLoading,否则物理/脚本门控永远跳过(卡死 Loading)
if (GetGameFlow().IsIn(GameFlowState::Loading))
{
if (std::filesystem::exists(m_ScenePath))
{
if (Serialization::SceneSerializer::Deserialize(
GetScene(), GetAssets(), m_ScenePath))
{
ENGINE_INFO("Game: scene loaded from {}", m_ScenePath);
}
else
{
ENGINE_ERROR("Game: scene load failed: {}",
m_ScenePath);
}
}
else
{
ENGINE_WARN("Game: scene file not found ({}), "
"starting empty", m_ScenePath);
}
GetGameFlow().CompleteLoading();
}
#endif
}
// ---- UI(全部委托 docking Editor;本文件零 ImGui 控件)----
void OnImGuiRender() override
{
#ifdef HELLO_EDITOR
m_Editor->Draw();
#endif
}
// ---- 事件 ----
// ---- 叠加层 UI(唯一允许出现 ImGui 控件的地方)----
void OnRenderOverlay() override
#ifdef HELLO_EDITOR
void OnWindowClose(const Event& event) override
{
if (!m_OverlayVisible) return;
ImGui::Begin("Scene Debug");
// 阴影光源方向:拖动滑条实时看影子变化
if (auto* light = m_KeyLight.TryGetComponent<LightComponent>()) {
ImGui::Text("Key Light (shadow caster)");
ImGui::SliderFloat3("direction",
&light->direction.x, -1.0f, 1.0f);
if (ImGui::Button("reset")) {
light->direction = glm::vec3(-0.5f, -0.6f, -0.35f);
}
}
ImGui::Separator();
ImGui::Text("fps: %.1f frame: %llu",
1.0f / GetDeltaTime(),
static_cast<unsigned long long>(GetFrameCount()));
ImGui::End();
// 退出确认走 Editor 的模态框(ImGui 必须可见才有 UI)
SetImGuiEnabled(true);
m_Editor->RequestQuit();
}
#endif
void OnKeyPress(const Event& event) override
{
if (event.key == GLFW_KEY_GRAVE_ACCENT) {
m_OverlayVisible = !m_OverlayVisible; // ` 反折叠加层
}
if (event.key == GLFW_KEY_F1) {
STATS.PrintComparison();
}
if (event.key == GLFW_KEY_ESCAPE) {
Close();
// 键码统一走 Input::Key(App 层零 GLFW 依赖)
#ifdef HELLO_EDITOR
if (event.key == static_cast<int>(Input::Key::GraveAccent))
{
SetImGuiEnabled(!GetImGuiEnabled());
}
#endif
}
void OnWindowResize(const Event& event) override
{
ENGINE_INFO("App: window resized to {}x{}", event.width, event.height);
}
private:
EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空
EntityHandle m_Ground{};
EntityHandle m_KeyLight{};
std::vector<EntityHandle> m_Spinners;
bool m_OverlayVisible = true;
f32 m_Time = 0.0f;
#ifdef HELLO_EDITOR
// Editor 注入:门面指针 + 反向能力回调(面板 → 沙盒)
void InitEditor()
{
EditorContext ctx;
ctx.renderer = &GetRenderer();
ctx.scene = &GetScene();
ctx.assets = &GetAssets();
ctx.app = this;
ctx.flow = &GetGameFlow();
ctx.editorCamera = &GetRenderer().GetEditorCamera();
// 编辑器态默认冻结:GameFlow 构造即 timeScale=0(Stopped 语义),
// timeScale 权威在 GameFlow,此处不再手设
m_Editor->Init(ctx);
// 输入归属:覆盖 EnableImGui 的全局 WantCapture* 谓词
//(全屏 dock host 让它恒真,输入全死)。改为 Editor 按帧
// 判定——Viewport hover/focus 拥有输入(Hazel/UE 语义);
// UI 隐藏(反引号)时 Draw 不再执行,直接全放行
GetWindow().SetInputCapture(
[this] { return GetImGuiEnabled() && m_Editor->BlockingKeyboard(); },
[this] { return GetImGuiEnabled() && m_Editor->BlockingMouse(); });
}
private:
std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>();
#else
// game 形态:启动场景路径(CWD = bin;编辑器 Save 的产物位置)
const char* m_ScenePath = "assets/scenes/sandbox.scene";
#endif
};
}
int main()
{
Logger::Initialize();
{
SandboxApp app;
try {
try
{
app.Run();
}
catch (const std::exception& e) {
catch (const std::exception& e)
{
ENGINE_FATAL("Exception: {}", e.what());
Logger::Shutdown();
return -1;
}
} // app 析构:成员(Device/Renderer/Window…)全部销毁完毕
Logger::Shutdown();
return 0;
}
+203
View File
@@ -0,0 +1,203 @@
//
// Follower.cpp:跟随者行为(NPC 自动跟随主角,UE AIController 的简化位)。
//
// 挂在每个 NPC 实体上(各自 CharacterController + Animation)。主角由
// TargetName 指定(默认 "player",按 NameComponent 名称匹配——引擎无实体
// 引用参数类型,名称匹配是标准做法)。逻辑分四层,与 WASDMove 同一套手感:
// - 距离闸门(迟滞):拉开到 StopDistance+ResumeGap 才起步,靠近到
// StopDistance 即停——"到附近就停止跟随",边界不抖动
// - 速度分档(迟滞):距离超过 RunDistance 改跑,回落到 0.85× 才退回
// 走——"距离越大走/跑越快"
// - 动量整形:真实速度逐帧逼近期望速度(起步渐快、到点刹停),帧率无关
// - 动画按真实速度分档(idle/walk/run),迟滞防抖
// 移动驱动走 PhysicsWorld 的 CharacterController 门面(水平喂意图速度,
// Y 归物理重力);无 CC 时回退直接平移。多个跟随者用 Offset 错开站位。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Physics/PhysicsWorld.h>
#include "ScriptLog.h"
#include <entt/entt.hpp>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cmath>
using namespace Engine;
namespace {
class Follower final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "Follower"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("TargetName", &m_TargetName, std::string("player"));
// 相对目标的站位偏移(世界系;多个跟随者错开,防叠一起)
pb.Field("Offset", &m_Offset, glm::vec3{0.0f});
pb.Field("WalkSpeed", &m_WalkSpeed, 3.0f); // 走路 m/s
pb.Field("RunSpeed", &m_RunSpeed, 6.0f); // 跑步 m/s
pb.Field("StopDistance", &m_StopDistance, 2.5f); // 到点停(米)
pb.Field("ResumeGap", &m_ResumeGap, 1.5f); // 再起步的距离余量
pb.Field("RunDistance", &m_RunDistance, 10.0f); // 超过即改跑(米)
pb.Field("TurnResponse", &m_TurnResponse, 10.0f);// 转向阻尼
pb.Field("AccelResponse", &m_AccelResponse, 6.0f); // 加速响应
pb.Field("StopResponse", &m_StopResponse, 12.0f); // 制动响应
// clip 按名引用(Unity anim.Play("Walk") 语义)
pb.Field("IdleClip", &m_IdleClip, std::string("idle"));
pb.Field("WalkClip", &m_WalkClip, std::string("walk"));
pb.Field("RunClip", &m_RunClip, std::string("run"));
}
void OnStart() override {
if (ResolveTarget()) {
SLOG_INFO("[Follower] 跟随目标 '{}' 就绪", m_TargetName);
} else {
SLOG_WARN("[Follower] 未找到目标 '{}'(按 NameComponent 匹配)",
m_TargetName);
}
}
void OnUpdate(float dt) override {
auto& reg = App().GetScene().GetRegistry();
// 目标失效(被销毁/未就绪)→ 重解析;仍无则静止待命
if (m_Target == entt::null || !reg.valid(m_Target)) {
m_Target = entt::null;
if (!ResolveTarget()) {
return;
}
}
const glm::vec3 tp =
reg.get<Engine::ECS::TransformComponent>(m_Target)
.GetPosition();
const glm::vec3 self = Transform().GetPosition();
glm::vec3 delta = (tp + m_Offset) - self;
delta.y = 0.0f; // 只取水平面(角色不俯仰)
const float dist = glm::length(delta);
// ---- 距离闸门(迟滞):到点停、拉开距离再起步 ----
if (!m_Moving) {
if (dist > m_StopDistance + m_ResumeGap) {
m_Moving = true;
}
} else if (dist <= m_StopDistance) {
m_Moving = false;
}
// ---- 速度分档(迟滞):远则跑、近则走 ----
glm::vec3 desired{0.0f};
if (m_Moving && dist > 1e-4f) {
if (!m_Sprinting && dist >= m_RunDistance) {
m_Sprinting = true;
} else if (m_Sprinting && dist <= m_RunDistance * 0.85f) {
m_Sprinting = false;
}
const float speed = m_Sprinting ? m_RunSpeed : m_WalkSpeed;
desired = (delta / dist) * speed;
}
// ---- 动量整形:真实速度逐帧逼近期望(起步渐快/到点刹停)----
const float response =
glm::length(desired) > 0.01f ? m_AccelResponse : m_StopResponse;
m_PlanarVel +=
(desired - m_PlanarVel) * (1.0f - std::exp(-response * dt));
if (glm::length(desired - m_PlanarVel) < 0.02f) {
m_PlanarVel = desired; // 贴齐,防低速蠕动
}
// ---- 驱动:角色控制器喂水平速度(Y 归物理);无则直接平移 ----
auto& phy = App().GetPhysics();
const bool hasCC =
TryGetComponent<Engine::ECS::CharacterControllerComponent>() !=
nullptr;
if (hasCC) {
const glm::vec3 cur = phy.GetCharacterVelocity(EntityBits());
phy.SetCharacterVelocity(
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
} else {
Transform().SetPosition(self + m_PlanarVel * dt);
}
// ---- 朝向:面向真实速度方向(模型默认 +Z),低速锁定 ----
if (glm::length(m_PlanarVel) > 0.3f) {
const float targetYaw =
std::atan2(m_PlanarVel.x, m_PlanarVel.z);
const glm::vec3 modelFwd =
Transform().GetRotationQuat() * glm::vec3(0.0f, 0.0f, 1.0f);
const float curYaw = std::atan2(modelFwd.x, modelFwd.z);
const float dyaw = std::atan2(std::sin(targetYaw - curYaw),
std::cos(targetYaw - curYaw));
const float k = 1.0f - std::exp(-m_TurnResponse * dt);
Transform().SetRotationQuat(
glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0)));
}
// ---- 动画:按真实速度分档(迟滞防临界抖)----
const float planarSpeed = glm::length(m_PlanarVel);
std::string target;
if (planarSpeed < 0.15f) {
target = m_IdleClip;
} else if (m_CurClip == m_RunClip) {
target = (planarSpeed >= m_RunSpeed * 0.7f) ? m_RunClip
: m_WalkClip;
} else {
target = (planarSpeed >= m_RunSpeed * 0.9f) ? m_RunClip
: m_WalkClip;
}
if (target != m_CurClip) {
if (auto* anim =
TryGetComponent<Engine::ECS::AnimationComponent>();
anim != nullptr && anim->Play(target, 0.35f)) {
m_CurClip = target; // 播放成功才记账(模型未就绪下帧重试)
}
}
}
private:
// 按 NameComponent 名称解析目标(跳过自身);失败返回 false
bool ResolveTarget() {
auto& reg = App().GetScene().GetRegistry();
auto view = reg.view<Engine::ECS::NameComponent>();
for (auto e : view) {
if (static_cast<u32>(entt::to_integral(e)) == EntityBits()) {
continue;
}
if (view.get<Engine::ECS::NameComponent>(e).name ==
m_TargetName) {
m_Target = e;
return true;
}
}
return false;
}
std::string m_TargetName = "player";
glm::vec3 m_Offset{0.0f};
float m_WalkSpeed = 3.0f;
float m_RunSpeed = 6.0f;
float m_StopDistance = 2.5f;
float m_ResumeGap = 1.5f;
float m_RunDistance = 10.0f;
float m_TurnResponse = 10.0f;
float m_AccelResponse = 6.0f;
float m_StopResponse = 12.0f;
std::string m_IdleClip = "idle";
std::string m_WalkClip = "walk";
std::string m_RunClip = "run";
entt::entity m_Target = entt::null; // 运行时状态(非参数)
bool m_Moving = false; // 距离闸门状态(迟滞)
bool m_Sprinting = false; // 速度档状态(迟滞)
glm::vec3 m_PlanarVel{0.0f}; // 平滑后的水平真实速度
std::string m_CurClip = "idle"; // 当前 clip(边沿检测基准)
};
ENGINE_REGISTER_SCRIPT(Follower)
} // namespace
+704
View File
@@ -0,0 +1,704 @@
//
// FractureLib.h:共享碎裂库(自 WallFracture.cpp 提取,行为逐行保持)。
//
// 碎裂语义 = 玻璃/陶瓷式("碎掉"),非爆炸飞散,碎片几何 = 2D
// Voronoi 分割:
// 碎裂瞬间在面上撒种子点(命中点周边加密 → 孔洞周围碎片小、
// 远处碎片大,玻璃碎的典型分布),对每颗种子做垂直平分线半平面
// 交集得到不规则凸多边形 cell,沿面厚挤出成凸棱柱——碎片在碎裂
// 前一刻完美拼合原面,碎开后沿裂纹分离。碰撞形状不走 Box,直接
// 用 MeshCollider(Dynamic → Jolt ConvexHull,数据源 = 网格 CPU
// 位置副本,PhysicsWorld::BuildMeshShape)→ 不规则碎片连碰撞体
// 都是真的,落地翻滚/堆积接触自然。
//
// 初速 = 径向 × 距离指数衰减(只有命中点周边的碎片被推动,远处
// 几乎原地悬置)+ 沿命中法线轻微外推(朝弹侧剥落)+ 微弱上抛
// ——重力是塌落的主导,整体呈"裂纹分离 → 孔洞塌落 → 碎块堆在
// 墙脚"的观感。
//
// 用法(宿主 = 挂在可碎实体上的脚本):
// Fracture::Burst burst_; // 碎片簿记 + 生命周期
// DeclareParams 直绑 burst_.p 各字段(pb.Field("Count",
// &burst_.p.count, ...))——参数必须与碎裂逻辑读的是同一对象,
// 绑到独立副本会让 Count/Lifetime 等全部失效
// HostView HV() { // 桥接(脚本自身成员函数内可访问
// return {App(), EntityBits(),// protected 基类设施,库内只走公共 API)
// [this](auto& n) { return Spawn(n); },
// [this](auto e) { Destroy(e); }};
// }
// OnCollisionEnter → burst_.TryFire(HV(), other, point, normal)
// OnUpdate → burst_.Tick(HV(), dt)
// OnDestroy → burst_.Shutdown(HV())
// 外观:默认继承宿主(墙)的生效材质(底色/贴图/透明),碎块与墙面
// 一致;要独立外观(玻璃靶 = 半透明 + 自发光)Fire 前置
// burst_.look.inheritFromHost = false 再填 baseColor/emissive
// HostView 每次调用现填(不存指针:组件存储重排/实例重建均安全)
//
// 占位局限(真碎片资产接入后本路径退役):碎片网格顶点按宿主世界
// 尺寸烘焙、碎片实体复用宿主 world pos/rot(scale 恒 1,防
// ConvexHull 顶点二次缩放)——宿主须无旋转摆放。
//
#ifndef HELLOVULKAN_FRACTURE_LIB_H
#define HELLOVULKAN_FRACTURE_LIB_H
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Types.h>
#include <Engine/Assets/AssetManager.h> // LoadModel(门面只前向声明)
#include <Engine/Renderer/Model.h> // ParsedModel/ParsedMesh(程序化碎片几何)
#include <Engine/Renderer/Vertex.h> // Vertex 聚合布局
#include <Engine/Physics/PhysicsWorld.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <algorithm>
#include <cmath>
#include <functional>
#include <random>
namespace Engine::Fracture {
// bits(PhysicsWorld 键)→ 实体(还原位模式;用前自行 valid 校验)
inline ECS::Entity EntityFromBits(u32 bits) {
return static_cast<ECS::Entity>(bits);
}
// 实体句柄 → bits(EntityBits() 只覆盖自身绑定实体,碎片簿记
// 需要任意实体的同款换算)
inline u32 BitsOf(const ECS::EntityHandle& e) {
return static_cast<u32>(
entt::to_integral(static_cast<entt::entity>(e)));
}
inline f32 Rand(f32 lo, f32 hi) {
thread_local std::mt19937 rng{std::random_device{}()};
return std::uniform_real_distribution<f32>(lo, hi)(rng);
}
// 碎片模型代序号(全局递增):每代碎片一组唯一缓存键 → 每次
// 命中都重新随机生成形状(命中点量化键会让同位置重复命中
// 复用首次几何——缓存去重把新几何丢弃,碎片就"固定"了)
inline u32& GenSeq() {
static u32 s = 0;
return s;
}
// ---- 宿主桥接(脚本侧现填;库内只走公共 API)----
// spawn/destroy 走脚本成员(内部才能访问 ScriptBehavior 的
// protected 底座),app/实体走数据。std::function 捕 this 无
// 分配(小对象优化),随调用栈存亡,无悬垂面
struct HostView {
Application& app;
u32 entityBits = 0;
ECS::Entity entity() const { return EntityFromBits(entityBits); }
std::function<ECS::EntityHandle(const std::string&)> spawn;
std::function<void(ECS::Entity)> destroy;
};
// ---- 2D Voronoi(碎裂裂纹)----
//
// cell = 相对其它种子的垂直平分线半平面交集,初始域 = 墙面矩形。
// Sutherland–Hodgman 凸多边形裁剪:保留靠近 p 的一侧
//(dot(a,x) <= b,a = normalize(q-p),b = dot(a,p)+|q-p|/2)。
// 输入多边形须 CCW(初始矩形即 CCW,裁剪保序)。
inline std::vector<glm::vec2> ClipByBisector(std::vector<glm::vec2> poly,
const glm::vec2& p,
const glm::vec2& q) {
const glm::vec2 d = q - p;
const f32 len = glm::length(d);
if (len < 1e-6f) return poly;
const glm::vec2 a = d / len;
const f32 b = glm::dot(a, p) + 0.5f * len;
std::vector<glm::vec2> out;
out.reserve(poly.size() + 1);
for (size_t i = 0; i < poly.size(); ++i) {
const glm::vec2& c = poly[i];
const glm::vec2& n = poly[(i + 1) % poly.size()];
const f32 dc = glm::dot(a, c) - b;
const f32 dn = glm::dot(a, n) - b;
if (dc <= 0.0f) out.push_back(c);
if ((dc < 0.0f) != (dn < 0.0f)) { // 边跨越裁剪线 → 求交点
const f32 t = dc / (dc - dn);
out.push_back(c + t * (n - c));
}
}
return out;
}
// 种子 p 的 Voronoi cell(域 = [-hw,hw]×[-hh,hh] 的墙面矩形)
inline std::vector<glm::vec2> VoronoiCell(const glm::vec2& p,
const std::vector<glm::vec2>& seeds,
f32 hw, f32 hh) {
std::vector<glm::vec2> poly = {
{-hw, -hh}, {hw, -hh}, {hw, hh}, {-hw, hh}};
for (const glm::vec2& q : seeds) {
if (glm::dot(q - p, q - p) < 1e-10f) continue; // 自身
poly = ClipByBisector(std::move(poly), p, q);
if (poly.size() < 3) return {}; // 退化(最小间距保护,罕见)
}
return poly;
}
// cell(CCW 凸多边形,墙局部 XY)沿 z ∈ [-hz, +hz] 挤出成棱柱:
// 顶/底面 fan 三角化 + 侧面逐边 quad,per-face 法线。返回 mesh
// 数据;cpuPositions 单独回传(ConvexHull 物理数据源)。
struct PrismMesh {
std::vector<Engine::Vertex> vertices;
std::vector<u32> indices;
std::vector<glm::vec3> cpuPositions;
};
// hw/hh = 宿主半尺寸:顶/底面 UV 由 cell 的墙局部坐标归一化到 [0,1]
//(墙面矩形 UV 约定,与立方体面 UV 对齐)——继承墙材质贴图时,碎片
// 贴图与墙面连续,不会各碎片自成一格
inline PrismMesh CellToPrism(const std::vector<glm::vec2>& cell, f32 hz,
f32 hw, f32 hh) {
PrismMesh m;
const size_t n = cell.size();
const size_t topBase = 0;
const size_t botBase = n;
m.vertices.reserve(n * 2);
const f32 invW = hw > 1e-6f ? 0.5f / hw : 0.0f;
const f32 invH = hh > 1e-6f ? 0.5f / hh : 0.0f;
const auto uvOf = [&](const glm::vec2& p) {
return glm::vec2{p.x * invW + 0.5f, p.y * invH + 0.5f};
};
for (const glm::vec2& p : cell) {
const Engine::Vertex v{
{p.x, p.y, hz}, {0.0f, 0.0f, 1.0f},
uvOf(p), {1.0f, 0.0f, 0.0f}};
m.vertices.push_back(v);
}
for (const glm::vec2& p : cell) {
const Engine::Vertex v{
{p.x, p.y, -hz}, {0.0f, 0.0f, -1.0f},
uvOf(p), {1.0f, 0.0f, 0.0f}};
m.vertices.push_back(v);
}
// top fan(CCW 从 +Z 看朝外)+ bottom fan(反向)
for (size_t i = 1; i + 1 < n; ++i) {
m.indices.push_back(topBase);
m.indices.push_back(topBase + i);
m.indices.push_back(topBase + i + 1);
m.indices.push_back(botBase);
m.indices.push_back(botBase + i + 1);
m.indices.push_back(botBase + i);
}
// 侧面:(i, N+i, N+i+1, i+1)——外法线 = CCW 边的右侧法向
for (size_t i = 0; i < n; ++i) {
const size_t j = (i + 1) % n;
const glm::vec2 e = cell[j] - cell[i];
const f32 el = glm::length(e);
const glm::vec3 sn = el > 1e-6f
? glm::vec3{e.y / el, -e.x / el, 0.0f}
: glm::vec3{0.0f, -1.0f, 0.0f};
const size_t b = m.vertices.size();
const Engine::Vertex q0{
{cell[i].x, cell[i].y, hz}, sn,
{0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}};
const Engine::Vertex q1{
{cell[i].x, cell[i].y, -hz}, sn,
{0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}};
const Engine::Vertex q2{
{cell[j].x, cell[j].y, -hz}, sn,
{1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}};
const Engine::Vertex q3{
{cell[j].x, cell[j].y, hz}, sn,
{1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}};
m.vertices.push_back(q0);
m.vertices.push_back(q1);
m.vertices.push_back(q2);
m.vertices.push_back(q3);
m.indices.insert(m.indices.end(),
{u32(b), u32(b + 1), u32(b + 2),
u32(b), u32(b + 2), u32(b + 3)});
}
// 物理凸包数据源(BuildMeshShape Dynamic 分支逐顶点收集)
m.cpuPositions.reserve(m.vertices.size());
for (const auto& v : m.vertices) {
m.cpuPositions.push_back(v.position);
}
return m;
}
// ---- 程序化纯色还原(引擎零改动:颜色只存在于伪路径里)----
// 程序化 Cube/Plane 把颜色烘进 1x1 sRGB 纹理,MaterialParams 留白、
// 内建材质快照也没有颜色字段,故从伪路径尾段 "r,g,b[,a]" 还原线性
// 底色——与 GPU 采样 sRGB 纹理的硬件线性化同式,直乘 baseColor 才等价
inline f32 SrgbByteToLinear(u8 c) {
const f32 s = static_cast<f32>(c) / 255.0f;
return s <= 0.04045f ? s / 12.92f
: std::pow((s + 0.055f) / 1.055f, 2.4f);
}
// "procedural://cube/0.5/140,92,72" → 取最后一个 '/' 之后的纯数字
// 逗号段;段数 < 3 或含非数字(如 halfSize 段)判失败
inline bool ParseProceduralColor(const std::string& path, glm::vec4& out) {
if (path.rfind("procedural://", 0) != 0) return false;
const size_t slash = path.find_last_of('/');
if (slash == std::string::npos) return false;
const std::string tail = path.substr(slash + 1);
int c[3] = {0, 0, 0};
int n = 0;
size_t pos = 0;
while (n < 3 && pos < tail.size()) {
const size_t comma = tail.find(',', pos);
const size_t end =
(comma == std::string::npos) ? tail.size() : comma;
if (end == pos) return false;
int v = 0;
for (size_t i = pos; i < end; ++i) {
const char ch = tail[i];
if (ch < '0' || ch > '9') return false;
v = v * 10 + (ch - '0');
}
c[n++] = v;
if (comma == std::string::npos) break;
pos = comma + 1;
}
if (n < 3) return false;
out = glm::vec4{SrgbByteToLinear(static_cast<u8>(c[0])),
SrgbByteToLinear(static_cast<u8>(c[1])),
SrgbByteToLinear(static_cast<u8>(c[2])), 1.0f};
return true;
}
// ---- 参数(原 WallFracture 全量参数,字段类型随 Inspector 控件)----
struct Params {
i32 count = 30; // Voronoi 种子数 = 碎片数
f32 seedRadius = 0.9f; // 加密半径 m
f32 minSeedDist = 0.12f; // 种子最小间距 m
f32 burstSpeed = 1.2f; // 近点径向初速 m/s
f32 falloff = 1.0f; // 距离衰减率 1/m
f32 normalPop = 0.4f; // 法线外推 m/s
f32 upBias = 0.3f; // 上抛偏置 m/s
f32 lifetime = 6.0f; // 碎片寿命(秒)
i32 maxFragments = 128; // 活碎片上限
f32 minImpactSpeed = 2.0f; // 触发阈值(m/s)
};
// ---- 碎片外观 ----
// 默认 inheritFromHost = true:碎块继承宿主(墙)的"生效材质"——
// 底色/贴图/透明/自发光全套沿用,碎掉后与墙面目视一致。宿主材质
// 取实体级覆盖(.material 资产)优先、模型内建材质兜底,与
// SceneRenderer 收集期的 resolve 同序
// 需要独立外观(玻璃靶 = 半透明 + 自发光等)时置 false,改用下面
// 的显式字段(Fire 前设置)
struct Appearance {
bool inheritFromHost = true; // true = 材质沿用宿主,忽略下列字段
glm::vec4 baseColor{140.0f / 255.0f, 92.0f / 255.0f,
72.0f / 255.0f, 1.0f};
bool transparent = false; // 透明绘制队列(玻璃碎片)
glm::vec3 emissive{0.0f}; // 自发光颜色(黑 = 不发光)
f32 emissiveStrength = 1.0f;
};
// ---- 碎裂执行体(宿主脚本持有;一次 Fire 管一整批碎片)----
class Burst {
public:
Params p; // 宿主 DeclareParams 直绑
Appearance look; // 碎片外观(Fire 前设置)
// 触发流程一次做完:一次性保护 → 对方校验(活 Dynamic +
// 速度阈值)→ 藏宿主 → 切分 → spawn 碎片群。未触发返回
// false(速度不够/重复触发),宿主无需再写判定
bool TryFire(const HostView& host,
u32 other, const glm::vec3& point,
const glm::vec3& normal) {
if (m_Fired) return false;
// 对方须"够快"且是可动的撞击来源,二选一:
// - 动态刚体(子弹/抛体):用实时线速度
// - 角色(CharacterVirtual,非刚体、无 RigidBodyComponent):
// 用"扫掠前"速度——扫掠中朝墙面的分量会被 Jolt 抵消,
// 实时速度往往已归零,会漏判(见
// PhysicsWorld::GetCharacterImpactVelocity)
// 速度低于阈值(静止贴靠/轻碰)则忽略
auto& registry = host.app.GetScene().GetRegistry();
const ECS::Entity otherE = EntityFromBits(other);
if (!registry.valid(otherE)) return false;
f32 speed = 0.0f;
if (const auto* otherRb =
registry.try_get<Engine::ECS::RigidBodyComponent>(
otherE)) {
if (otherRb->type !=
Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
return false; // 静态/运动学体不算撞击
}
speed = glm::length(
host.app.GetPhysics().GetLinearVelocity(other));
} else if (registry.all_of<
Engine::ECS::CharacterControllerComponent>(
otherE)) {
// 玩家/角色撞击:角色碰撞事件由 PhysicsWorld 合成
// (CharacterVirtual 不进 ContactListener)
speed = glm::length(
host.app.GetPhysics().GetCharacterImpactVelocity(other));
} else {
return false; // 无物理来源(纯 Transform 实体)
}
if (speed < p.minImpactSpeed) return false;
m_Fired = true;
Fire(host, point, normal);
return true;
}
// 每帧簿记(宿主 OnUpdate 直调;未 Fire 过无簿记可理)
void Tick(const HostView& host, f32 dt) {
if (!m_Fired) return;
ApplyPendingUnload(host);
ApplyPendingVelocities(host);
AgeAndCleanup(host, dt);
}
// 宿主销毁(PIE stop 换血 / 手动删除):碎片实体由场景清亡
// 处理,独占碎片模型统一批量回收(模型不被快照恢复的场景
// 引用——唯一代键;批量接口内部 GPU 停飞后再销毁)
void Shutdown(const HostView& host) {
if (!m_Fired) return; // 从未 Fire:无碎片无模型
std::vector<std::string> paths;
paths.reserve(m_Fragments.size() + m_PendingUnload.size());
for (const Fragment& f : m_Fragments) {
if (!f.modelPath.empty()) paths.push_back(f.modelPath);
}
m_Fragments.clear();
paths.insert(paths.end(),
std::make_move_iterator(m_PendingUnload.begin()),
std::make_move_iterator(m_PendingUnload.end()));
m_PendingUnload.clear();
if (!paths.empty()) {
host.app.GetAssets().UnloadModelsByPaths(paths);
}
}
bool Fired() const { return m_Fired; }
private:
// ---- 触发碎裂:藏宿主 + Voronoi 切分 + spawn 碎片群 ----
void Fire(const HostView& host,
const glm::vec3& hitPoint, const glm::vec3& hitNormal) {
auto& registry = host.app.GetScene().GetRegistry();
const ECS::Entity self = host.entity();
// 藏宿主:渲染/拾取跳过 + body 对账销毁(组件级 active,
// 与 Unity Renderer.enabled + Rigidbody 摘除同语义)
if (auto* mr =
registry.try_get<Engine::ECS::MeshRendererComponent>(
self)) {
mr->active = false;
}
if (auto* rb =
registry.try_get<Engine::ECS::RigidBodyComponent>(
self)) {
rb->active = false;
}
// 宿主几何:碎片网格顶点按宿主世界尺寸烘焙(面 = XY 平面,
// 厚度沿 Z;宿主须无旋转摆放,见头注释占位局限)
const auto& wall = registry.get<Engine::ECS::TransformComponent>(
self);
const glm::vec3 wallPos = wall.GetWorldPosition();
const glm::vec3 wallSize = wall.GetWorldScale();
const f32 hw = 0.5f * wallSize.x;
const f32 hh = 0.5f * wallSize.y;
const f32 hz = 0.5f * wallSize.z;
// ---- 碎片外观:解析宿主(墙)的生效材质 ----
// 与 SceneRenderer 收集期 resolve 同序:实体级覆盖
//(.material 资产,按 mesh 下标查表)优先,模型内建材质兜底
//(MeshMaterialInfo 为权威只读描述)。不继承则碎片只剩默认
// 砖色,与墙面目视不符
Engine::MaterialAssetData effMat;
glm::vec4 effSolid{0.0f}; // 程序化纯色 albedo(a>0 = 有效)
std::string hostOverridePath; // 宿主 .material 覆盖(空 = 内建材质)
bool hasEffMat = false;
if (look.inheritFromHost) {
const auto* mr = registry.try_get<
Engine::ECS::MeshRendererComponent>(self);
const int slot =
(mr && mr->meshIndex >= 0) ? mr->meshIndex : 0;
if (mr) {
const auto oIt =
mr->materialOverrides.find(static_cast<u32>(slot));
if (oIt != mr->materialOverrides.end()) {
if (auto* asset =
host.app.GetAssets().FindMaterialAsset(
oIt->second)) {
effMat = asset->GetData();
hostOverridePath = oIt->second;
hasEffMat = true;
}
}
}
if (!hasEffMat && mr && mr->model.Valid()) {
if (auto* model =
host.app.GetAssets().GetModel(mr->model)) {
const auto& infos = model->GetMeshMaterialInfos();
if (slot < static_cast<int>(infos.size())) {
effMat.texPaths = infos[slot].texPaths;
effMat.params = infos[slot].params;
effMat.transparent = infos[slot].transparent;
hasEffMat = true;
}
}
// 程序化纯色墙:颜色在生成的 1x1 纹理里(快照无颜色
// 字段),改由脚本从模型伪路径还原线性底色
ParseProceduralColor(
host.app.GetAssets().Models().Path(mr->model),
effSolid);
}
}
// 命中点 → 宿主局部(逆旋转 + 平移),XY 即裂纹平面坐标
const glm::quat invRot = glm::conjugate(
wall.GetWorldRotationQuat());
const glm::vec3 localHit = invRot * (hitPoint - wallPos);
const glm::vec2 uvHit{localHit.x, localHit.y};
// ---- 种子:40% 命中点半径内加密 + 60% 全面均匀,最小
// 间距拒绝采样(防碎片退化为细条)----
const u32 count = std::clamp(p.count, 4, 96);
const u32 nearCount = count * 2 / 5;
std::vector<glm::vec2> seeds;
seeds.reserve(count);
const f32 minD2 = p.minSeedDist * p.minSeedDist;
auto tryPush = [&](const glm::vec2& s) {
if (s.x < -hw || s.x > hw || s.y < -hh || s.y > hh) return;
for (const glm::vec2& e : seeds) {
if (glm::dot(e - s, e - s) < minD2) return;
}
seeds.push_back(s);
};
// 命中点周边:均匀盘采样(sqrt(Rand) 面积均匀)
for (u32 i = 0; i < nearCount * 12 && seeds.size() < nearCount;
++i) {
const f32 a = Rand(0.0f, glm::two_pi<f32>());
const f32 r = p.seedRadius * std::sqrt(Rand(0.0f, 1.0f));
tryPush(uvHit + r * glm::vec2{std::cos(a), std::sin(a)});
}
// 全面均匀补足(同款拒绝采样)
for (u32 i = 0; i < count * 12 && seeds.size() < count; ++i) {
tryPush({Rand(-hw, hw), Rand(-hh, hh)});
}
if (seeds.size() < 4) return; // 面太小采样失败:不碎
// ---- 每种子 → cell → 棱柱 mesh → ParsedModel ----
// 唯一代键(见 GenSeq 注释):形状每次随机;模型回收走
// Fragment.modelPath + 待卸载队列(ApplyPendingUnload)
const std::string keyBase =
"procedural://vorfrag/" +
std::to_string(GenSeq()++) + '/';
auto& assets = host.app.GetAssets();
std::vector<Engine::ModelRef> refs;
std::vector<glm::vec3> centroids; // 碎片质心(世界系,初速径向用)
std::vector<std::string> paths; // 模型缓存键(回收用,与 refs 对齐)
refs.reserve(seeds.size());
centroids.reserve(seeds.size());
paths.reserve(seeds.size());
for (size_t s = 0; s < seeds.size(); ++s) {
const std::vector<glm::vec2> cell =
VoronoiCell(seeds[s], seeds, hw, hh);
if (cell.size() < 3) {
refs.push_back({}); // 退化 cell:占位,spawn 跳过
centroids.emplace_back();
paths.emplace_back();
continue;
}
PrismMesh pm = CellToPrism(cell, hz, hw, hh);
glm::vec2 c2{0.0f};
for (const glm::vec2& q : cell) c2 += q;
c2 /= static_cast<f32>(cell.size());
centroids.push_back(
wallPos + wall.GetWorldRotationQuat() *
glm::vec3{c2.x, c2.y, 0.0f});
Engine::ParsedModel parsed;
parsed.path = keyBase + std::to_string(s);
Engine::ParsedMesh& mesh =
parsed.meshes.emplace_back();
mesh.vertices = std::move(pm.vertices);
mesh.lods.push_back(std::move(pm.indices));
mesh.cpuPositions = std::move(pm.cpuPositions);
mesh.name = "frag" + std::to_string(s);
if (hasEffMat) {
// 继承墙的生效材质:底色/贴图/UV 变换/透明全套沿用,
// 碎块与墙面目视一致(UV 已在 CellToPrism 归一到墙面)
mesh.params = effMat.params;
mesh.texPaths = effMat.texPaths;
mesh.transparent = effMat.transparent;
// 程序化纯色墙(无贴图路径):用脚本解析出的线性底色当
// baseColor——碎片 albedo 走默认白纹理,直乘即该色;
// 不走这步会退回 MaterialParams 的白色默认值
if (effSolid.a > 0.0f) {
mesh.params.baseColor = effSolid;
}
} else {
// 无宿主材质可继承(或 look.inheritFromHost=false):
// 用显式外观(玻璃靶 = 半透明 + 自发光)
mesh.params.baseColor = look.baseColor;
mesh.transparent = look.transparent;
mesh.params.emissiveFactor =
glm::vec4{look.emissive, 1.0f};
mesh.params.surface.z = look.emissiveStrength;
}
const Engine::ModelRef ref =
assets.LoadModel(parsed.path, std::move(parsed));
refs.push_back(ref);
paths.push_back(parsed.path);
}
// ---- spawn 碎片实体:pos/rot 复用宿主位姿(mesh 已真
// 尺寸,scale 恒 1 防 ConvexHull 顶点二次缩放),碎裂
// 瞬间完美拼合原面,动起来后沿裂纹自然分离 ----
for (size_t s = 0; s < refs.size(); ++s) {
if (!refs[s].Valid()) continue;
auto frag = host.spawn("fragment");
auto& tr = frag.GetComponent<
Engine::ECS::TransformComponent>();
tr.SetPosition(wallPos);
tr.SetRotationQuat(wall.GetWorldRotationQuat());
tr.SetScale(glm::vec3{1.0f});
auto& fmr =
frag.AddComponent<
Engine::ECS::MeshRendererComponent>();
fmr.model = refs[s];
// 宿主墙用 .material 覆盖时,碎块直接复用同一份资产:外观
//(含透明通道)与墙完全一致,且不需要引擎改判定
if (!hostOverridePath.empty()) {
fmr.materialOverrides[0] = hostOverridePath;
}
// Dynamic MeshCollider → ConvexHull(数据源 = 刚上传
// 的 cpuPositions;Box 字段不参与 Mesh 形状)
auto& frb =
frag.AddComponent<
Engine::ECS::RigidBodyComponent>();
frb.type =
Engine::ECS::RigidBodyComponent::BodyType::Dynamic;
frb.collider =
Engine::ECS::RigidBodyComponent::ColliderType::Mesh;
frb.mass = 0.3f;
frb.friction = 0.7f;
frb.restitution = 0.05f;
// 碎裂初速(玻璃式,非爆炸):径向 = 碎片质心 - 命中
// 点,离命中点越远越接近静止(指数衰减)——只有孔洞
// 周边的碎片被冲击波推动;法线外推让碎片朝弹侧剥出
glm::vec3 dir = centroids[s] - hitPoint;
const f32 len = glm::length(dir);
if (len < 1e-3f) {
dir = glm::vec3{Rand(-1.0f, 1.0f), 0.6f,
Rand(-1.0f, 1.0f)};
} else {
dir /= len;
}
const f32 falloff = std::exp(-len * p.falloff);
const glm::vec3 vel =
dir * p.burstSpeed * falloff *
Rand(0.4f, 1.1f) +
hitNormal * p.normalPop * Rand(0.3f, 0.8f) +
glm::vec3{0.0f, p.upBias * Rand(0.0f, 0.5f),
0.0f};
// 总量上限:超限立即回收最老碎片(防泄漏的兜底闸;
// 寿命清理才是常态回收)。模型延迟一帧卸载
const u32 bits = BitsOf(frag);
if (m_Fragments.size() >=
static_cast<size_t>(p.maxFragments)) {
host.destroy(EntityFromBits(m_Fragments.front().bits));
if (!m_Fragments.front().modelPath.empty()) {
m_PendingUnload.push_back(
std::move(m_Fragments.front().modelPath));
}
m_Fragments.erase(m_Fragments.begin());
}
m_Fragments.push_back(
Fragment{bits, vel, 0.0f, false, paths[s]});
}
}
// ---- 模型回收:延迟一帧 + 批量停飞卸载(Destroy 是帧尾延迟,
// 碎片实体本帧渲染收集仍会引用模型;下一帧实体已灭,但上一帧
// 提交的命令缓冲仍可能引用顶点缓冲/sampler——批量接口先
// WaitDeviceIdle 再销毁,与资产重建换绑同一引擎先例)----
void ApplyPendingUnload(const HostView& host) {
if (m_PendingUnload.empty()) return;
host.app.GetAssets().UnloadModelsByPaths(m_PendingUnload);
m_PendingUnload.clear();
}
// ---- 施速待办:body 建好(bodyValid)即注入,勿重复 ----
void ApplyPendingVelocities(const HostView& host) {
auto& registry = host.app.GetScene().GetRegistry();
for (auto it = m_Fragments.begin();
it != m_Fragments.end();) {
if (it->applied) { ++it; continue; }
const ECS::Entity e = EntityFromBits(it->bits);
if (!registry.valid(e)) { // 实体没了(外部销毁)
// 模型是独占资源(一代一键),实体没了模型也回收
if (!it->modelPath.empty()) {
m_PendingUnload.push_back(
std::move(it->modelPath));
}
it = m_Fragments.erase(it);
continue;
}
const auto* rb = registry.try_get<
Engine::ECS::RigidBodyComponent>(e);
if (rb && rb->bodyValid) {
host.app.GetPhysics().SetLinearVelocity(
it->bits, it->velocity);
it->applied = true;
}
++it; // body 未建:留待下一帧(对账晚一帧必到)
}
}
// ---- 寿命清理:到期 Destroy(延迟队列,遍历安全)----
void AgeAndCleanup(const HostView& host, f32 dt) {
auto& registry = host.app.GetScene().GetRegistry();
for (auto it = m_Fragments.begin();
it != m_Fragments.end();) {
it->age += dt;
if (it->age < p.lifetime) { ++it; continue; }
const ECS::Entity e = EntityFromBits(it->bits);
if (registry.valid(e)) {
host.destroy(e);
}
// 模型延迟一帧卸载(本帧渲染收集还引用它)
if (!it->modelPath.empty()) {
m_PendingUnload.push_back(std::move(it->modelPath));
}
it = m_Fragments.erase(it);
}
}
struct Fragment {
u32 bits = 0; // PhysicsWorld 键(实体位模式)
glm::vec3 velocity{0.0f};// 待施初速(body 建好后注入)
f32 age = 0.0f; // 存活秒数(寿命清理用)
bool applied = false; // 初速已注入
std::string modelPath; // 独占模型缓存键(回收时按路径卸载)
};
bool m_Fired = false; // 一次性触发保护
std::vector<Fragment> m_Fragments; // 活碎片簿记
// 待卸载模型路径(碎片销毁的下一帧执行——Destroy 帧尾延迟,
// 本帧渲染收集仍引用模型,同帧卸载会渲染悬空指针)
std::vector<std::string> m_PendingUnload;
};
} // namespace Engine::Fracture
#endif //HELLOVULKAN_FRACTURE_LIB_H
+46
View File
@@ -0,0 +1,46 @@
//
// OrbitMove.cpp:参数管线验证样本——实体绕中心点圆周运动。
//
// 参数全链路:DeclareParams 声明(Speed/Radius/Center)→ Inspector
// 自动出控件 → 随场景序列化 → 实例化/RefreshParams 灌值。PIE 中拖
// Radius 当帧生效;Stop 由快照天然回滚。
// 未声明成员(m_Angle)= 纯运行时状态,不进 Inspector 不落盘。
//
#include <Engine/Scripting/Script.h>
#include <glm/glm.hpp>
#include <cmath>
namespace {
class OrbitMove final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "OrbitMove"; }
// 参数声明(Inspector 控件 + 序列化 + 灌值共用 schema)
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("Speed", &m_Speed, 1.0f); // 角速度(弧度/秒)
pb.Field("Radius", &m_Radius, 3.0f); // 轨道半径
pb.Field("Center", &m_Center, glm::vec3{0.0f}); // 轨道中心
}
void OnUpdate(float dt) override {
m_Angle += m_Speed * dt;
// 直接驱动 Transform(相对父空间的圆周;无父 = 世界)
Transform().SetPosition(
m_Center + glm::vec3(std::cos(m_Angle) * m_Radius, 0.0f,
std::sin(m_Angle) * m_Radius));
}
private:
float m_Speed = 1.0f;
float m_Radius = 3.0f;
glm::vec3 m_Center{0.0f};
float m_Angle = 0.0f; // 运行时相位(非参数)
};
ENGINE_REGISTER_SCRIPT(OrbitMove);
} // namespace
+88
View File
@@ -0,0 +1,88 @@
//
// PrefabTest.cpp:阶段2 验收脚本——脚本侧 Instantiate(prefab) 运行时
// 实例化链路。
//
// PrefabTest(挂任意空实体,作为实例化锚点):
// OnStart 按参数路径实例化一次 prefab;Interval > 0 时周期性再实例化
// (多实例验收:每次 UUID 全量再生、物理体独立、沿 X 向排开下落)。
//
// 覆盖的引擎设施:
// - ScriptBehavior::Instantiate(path, pos, rot)(Unity Object.
// Instantiate 同位)→ SceneSerializer::InstantiatePrefab
// - prefab 子树含 RigidBody/ScriptComponent 时由 PhysicsWorld/
// ScriptRuntime 下一帧对账消化(与 Spawn 同语义)
//
// 验收步骤:
// 1. 编辑器搭一个实体子树(如 Dynamic 盒刚体 + MeshRenderer 的箱子,
// 可再挂个子实体验证层级一起走)→ Outliner 右键 Save As Prefab
// (落盘 assets/prefabs/<名字>.prefab)
// 2. 建空实体挂 PrefabTest,参数 PrefabPath 填该路径 → Play:
// 锚点处出现实例;Interval 设 1.0 可看连续多实例物理下落互不串扰
// 3. 编辑态也可从 Content Browser 把 .prefab 拖进 Outliner(节点=
// 挂其下,空白区=挂根)或双击直接实例化——UUID 独立、资产引用有效
//
#include <Engine/Scripting/Script.h>
#include <glm/glm.hpp>
using namespace Engine; // f32 / ScriptBehavior 等免前缀(对齐 TestShoot)
namespace {
using Engine::Scripting::ParamBuilder;
class PrefabTest final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "PrefabTest"; }
void DeclareParams(ParamBuilder& pb) override {
// prefab 资产路径(工作目录相对,同场景/资产管理约定)
pb.Field("PrefabPath", &m_PrefabPath,
std::string{"assets/prefabs/crate.prefab"});
// 周期实例化间隔(秒;<=0 = 仅 OnStart 实例化一次)
pb.Field("Interval", &m_Interval, 0.0f);
// 相邻实例的 X 向排开间距(多实例不叠在一起)
pb.Field("SpreadX", &m_SpreadX, 2.0f);
}
void OnStart() override {
m_Timer = m_Interval;
SpawnInstance();
}
void OnUpdate(f32 dt) override {
if (m_Interval <= 0.0f) {
return;
}
m_Timer -= dt;
if (m_Timer <= 0.0f) {
m_Timer = m_Interval;
SpawnInstance();
}
}
private:
void SpawnInstance() {
// 锚点实体位姿 + 排开偏移;旋转 = 锚点自身旋转
const auto& tr = Transform();
const glm::vec3 pos =
tr.GetPosition() + glm::vec3{m_SpreadX * f32(m_Count),
0.0f, 0.0f};
const glm::quat rot = tr.GetRotationQuat();
if (Instantiate(m_PrefabPath, pos, rot) == entt::null) {
return; // 路径无效/解析失败:日志已在序列化器里打过
}
++m_Count;
}
std::string m_PrefabPath;
f32 m_Interval = 0.0f;
f32 m_SpreadX = 2.0f;
f32 m_Timer = 0.0f;
int m_Count = 0; // 已实例化份数(排开偏移基准)
};
ENGINE_REGISTER_SCRIPT(PrefabTest)
} // namespace
+52
View File
@@ -0,0 +1,52 @@
//
// ScriptDebug.h:脚本侧力学调试封装(Unity Gizmos/Debug.DrawRay 的
// 游戏内同位物)。把调试惯例固化成默认值,脚本不用记参数序:
// - width = 像素(≥1),默认 3px
// - duration 恒 0(OnUpdate 每帧重画,事件类需求再裸调 Debug::)
// - depthTest 恒 false(跟角色跑的线被模型遮挡就失去意义)
// - headSize = 世界单位(箭头翼头空间尺寸),默认 0.08
//
#pragma once
#include <Engine/Debug/DebugDraw.h>
#include <glm/glm.hpp>
namespace ScriptDbg {
// 基础箭头:colorHex 语义色见下(重力红/支撑黄/意图蓝/真实绿)
inline void Arrow(const glm::vec3& from, const glm::vec3& to,
Engine::u32 colorHex, float width = 3.0f) {
Engine::Debug::Arrow(from, to, Engine::Debug::Rgb(colorHex),
/*headSize*/ 0.08f, /*width*/ width,
/*duration*/ 0.0f, /*depthTest*/ false);
}
// 重力:anchor=脚底,len=视觉长度。建议 ∝ gravityScale,且与支撑
// 力同长——静置时红(下)黄(上)等长 = 力平衡,一眼可读
inline void Gravity(const glm::vec3& anchor, float len) {
Arrow(anchor, anchor - glm::vec3(0.0f, len, 0.0f), 0xDC3C3C);
}
// 接触反力(法向支撑):只在有接触时存在——空中传 len=0 内部直接
// 不画(力学语义:离地即失支撑)
inline void Support(const glm::vec3& anchor, float len) {
if (len > 1e-3f) {
Arrow(anchor, anchor + glm::vec3(0.0f, len, 0.0f), 0xD9C33C);
}
}
// 加速度(深橙):脚本侧差分出的测量加速度(F=ma 的 a)。scale =
// m/s² → 世界长度换算;mag 低于 minMag 视为静置/匀速("没有线"=
// 没有加速度,本身就是信息),len 钳到 maxLen 防冲击尖峰爆屏
inline void Accel(const glm::vec3& anchor, const glm::vec3& accel,
float scale = 0.05f, float maxLen = 2.0f) {
const float mag = glm::length(accel);
if (mag > 0.1f) {
const float len = glm::min(mag * scale, maxLen);
Arrow(anchor, anchor + (accel / mag) * len, 0xE0640A);
}
}
} // namespace ScriptDbg
+17
View File
@@ -0,0 +1,17 @@
//
// ScriptLog.h:脚本日志门面(ScriptDbg 的文字版同位物)。
//
// 脚本打日志统一走这里——直接转发引擎 spdlog 默认 logger(终端
// + HelloVulkan.log + 编辑器 Log Panel 三处同收)。约定:消息开头
// 写 [脚本名] 前缀(面板过滤好定位):
// ScriptLog::Info("[WASDMove] jump");
//
#pragma once
#include <Engine/Core/Log.h>
#define SLOG_TRACE(...) ::spdlog::trace(__VA_ARGS__)
#define SLOG_DEBUG(...) ::spdlog::debug(__VA_ARGS__)
#define SLOG_INFO(...) ::spdlog::info(__VA_ARGS__)
#define SLOG_WARN(...) ::spdlog::warn(__VA_ARGS__)
#define SLOG_ERROR(...) ::spdlog::error(__VA_ARGS__)
+45
View File
@@ -0,0 +1,45 @@
//
// Spinner.cpp:C++ 原生脚本示范——Y 轴匀速自转。
//
// 写脚本四步:
// 1. 新建 .cpp 放本目录(CMake 自动进构建)
// 2. 继承 ScriptBehavior,覆写 Name/OnCreate/OnUpdate/OnDestroy
// 3. DeclareParams 声明可调参数(Inspector 自动出控件 + 随场景
// 序列化 + 实例化灌值)
// 4. 文件级 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 DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("Speed", &m_Speed, 90.0f); // 度/秒
}
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; // 度/秒(声明默认;实例化期被存档值覆盖)
};
ENGINE_REGISTER_SCRIPT(Spinner);
} // namespace
+226
View File
@@ -0,0 +1,226 @@
//
// TestShoot.cpp:阶段1 验收脚本——脚本侧 spawn/destroy 底座 + 碰撞事件
// 转发的端到端链路。
//
// 三个脚本协作(放一个文件方便对照):
// TestShoot(挂任意实体,如相机实体):
// 鼠标左键上升沿 → 从编辑器相机沿视线 spawn 子弹实体
// (Dynamic 球刚体 + Bullet 脚本项,枪口初速经参数传入)
// Bullet(子弹实体,由 TestShoot 运行时装配):
// OnCreate 施加枪口初速(body 已由当帧 Step 对账建好);
// OnCollisionEnter → 命中点 spawn ImpactFx + 自毁;
// 超时自毁防飞出地图泄漏;存活期画调试十字 = 弹道可视化
// ImpactFx(特效实体,由 Bullet 运行时装配,空实体 + 调试点标):
// 扩张十字 + 白→红渐隐,寿命尽自毁——占位特效
//
// 覆盖的引擎设施:
// - Spawn()/Destroy():ScriptBehavior 实体底座(Scene::CreateEntity/
// DestroyEntity 延迟队列)
// - 组件运行时装配:RigidBody(PhysicsWorld 下一帧对账建 body)、
// ScriptComponent(ScriptRuntime 下一帧对账建实例)
// - OnCollisionEnter/Exit:PhysicsWorld::GetEvents → ScriptRuntime
// 帧首派发
//
// 验收步骤:编辑器给任意实体挂 TestShoot → Play → RMB 飞行视角瞄准
// 墙体 → 左键射击:弹道点飞出 → 撞墙瞬间原地绽放十字特效、子弹消失。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Input.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Debug/DebugDraw.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h>
#include <glm/glm.hpp>
using namespace Engine; // Input / Debug 门面
namespace {
// ---- 子弹脚本(TestShoot 运行时挂载;不手动挂场景)----
class Bullet final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "Bullet"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
// 枪口初速(世界系 m/s):TestShoot spawn 时按视线方向写进
// 脚本项参数——数据经参数层传递,与序列化同一条路
pb.Field("Velocity", &m_Velocity, glm::vec3{0.0f});
pb.Field("Lifetime", &m_Lifetime, 5.0f); // 超时自毁(秒)
}
void OnCreate() override {
// body 已由本帧 Step 的 SyncScene 建好(spawn 发生在上帧
// OnUpdate):这里施加初速,物理从下步起按弹道积分
if (glm::length(m_Velocity) > 1e-4f) {
App().GetPhysics().SetLinearVelocity(EntityBits(), m_Velocity);
}
}
void OnUpdate(f32 dt) override {
m_Age += dt;
if (m_Age >= m_Lifetime) {
Destroy(static_cast<Engine::ECS::Entity>(EntityBits()));
return;
}
// 弹道可视化:命中点即弹着点(橙十字 = 弹头,速度线 = 尾迹)
const glm::vec3 pos = Transform().GetPosition();
const glm::vec3 vel =
App().GetPhysics().GetLinearVelocity(EntityBits());
Debug::Cross(pos, 0.09f, Debug::Rgb(0xFF8C1A),
/*width*/ 2.0f, /*duration*/ 0.0f,
/*depthTest*/ false);
if (glm::length(vel) > 0.5f) {
Debug::Arrow(pos, pos + vel * 0.03f, Debug::Rgb(0xFFB366),
/*headSize*/ 0.04f, /*width*/ 2.0f,
/*duration*/ 0.0f, /*depthTest*/ false);
}
}
void OnCollisionEnter(u32 other, const glm::vec3& point,
const glm::vec3& normal) override {
// 防重入:Destroy 是帧尾延迟执行,命中帧内子弹与碎片群
// 可产生多对接触事件(碎片云一块挨一块),无标志会一帧
// 爆出 N 个特效(同 WallFracture m_Fired 的一次性触发模式)
if (m_Hit) return;
m_Hit = true;
(void)other; (void)normal; // 法线留给将来定向溅射
// 命中特效:空实体锚在命中点,ImpactFx 脚本项由下一帧对账
// 实例化(Spawn 即时建实体,脚本实例化天然晚一帧)
auto fx = Spawn("impact");
fx.GetComponent<Engine::ECS::TransformComponent>()
.SetPosition(point);
auto& sc = fx.AddComponent<Engine::ECS::ScriptComponent>();
Engine::ECS::ScriptComponent::Item item;
item.name = "ImpactFx";
sc.items.push_back(std::move(item));
// 自毁:延迟队列,帧尾统一执行(事件派发循环安全)
Destroy(static_cast<Engine::ECS::Entity>(EntityBits()));
}
private:
glm::vec3 m_Velocity{0.0f};
f32 m_Lifetime = 5.0f;
f32 m_Age = 0.0f;
bool m_Hit = false; // 首次命中置位(防同帧多次接触重复特效)
};
// ---- 命中特效脚本(Bullet 运行时挂载;空实体 + 调试点标占位)----
class ImpactFx final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "ImpactFx"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("Duration", &m_Duration, 0.4f); // 特效寿命(秒)
}
void OnUpdate(f32 dt) override {
m_Age += dt;
if (m_Age >= m_Duration) {
Destroy(static_cast<Engine::ECS::Entity>(EntityBits()));
return;
}
// 扩张十字 + 白→红渐隐(占位:将来换 VFX/粒子系统)
const f32 t = m_Age / m_Duration;
const glm::vec3 pos = Transform().GetPosition();
Debug::Cross(pos, 0.12f + t * 0.55f,
glm::vec4{1.0f, 1.0f - t, 1.0f - t, 1.0f},
/*width*/ 3.0f, /*duration*/ 0.0f,
/*depthTest*/ false);
}
private:
f32 m_Duration = 0.4f;
f32 m_Age = 0.0f;
};
// ---- 枪口脚本(验收主体:挂到任意实体,位置/朝向取编辑器相机)----
class TestShoot final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "TestShoot"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("BulletSpeed", &m_BulletSpeed, 25.0f); // 枪口初速 m/s
pb.Field("MuzzleOffset", &m_MuzzleOffset, 0.6f); // 出膛距离 m
pb.Field("FireCooldown", &m_FireCooldown, 0.15f); // 最小射击间隔 s
pb.Field("BulletRadius", &m_BulletRadius, 0.12f); // 弹头碰撞半径 m
// 半径 0.12 的反隧穿账:物理离散检测(无 CCD),检测带 =
// 半径 + 障碍半厚;薄板(如 0.2m 墙)窗口 2*(0.12+0.1)=0.44m
// > 每步进位移 0.42m(25 m/s @ 1/60),垂直命中保证检出。
// 半径越小高速弹越易整步跨过薄碰撞体(无接触=无事件)
}
void OnUpdate(f32 dt) override {
m_Cooldown -= dt;
// 左键上升沿触发(电平查询 + 上帧状态对比,Unity
// GetMouseButtonDown 边沿语义的手工等价物)
const bool down =
Input::IsMouseButtonDown(Input::MouseButton::Left);
const bool fire = down && !m_FireWasDown &&
m_Cooldown <= 0.0f;
m_FireWasDown = down;
if (fire) {
SpawnBullet();
m_Cooldown = m_FireCooldown;
}
}
private:
void SpawnBullet() {
// 枪口 = 编辑器相机视线前方(编辑形态 RMB 飞行瞄准;
// 与 WASDMove 的朝向来源一致)
const auto& cam =
App().GetRenderer().GetEditorCamera().GetTransform();
const glm::vec3 fwd =
cam.GetRotationQuat() * glm::vec3(0.0f, 0.0f, -1.0f);
const glm::vec3 muzzle =
cam.GetWorldPosition() + fwd * m_MuzzleOffset;
// ---- 子弹实体装配(spawn 底座 + 组件运行时装配演示)----
auto bullet = Spawn("bullet");
auto& tr = bullet.GetComponent<Engine::ECS::TransformComponent>();
tr.SetPosition(muzzle);
tr.SetRotationQuat(cam.GetRotationQuat());
// Dynamic 球刚体:参数给默认档(轻质量/低弹性,命中即停)
auto& rb = bullet.AddComponent<Engine::ECS::RigidBodyComponent>();
rb.type = Engine::ECS::RigidBodyComponent::BodyType::Dynamic;
rb.collider =
Engine::ECS::RigidBodyComponent::ColliderType::Sphere;
rb.sphereRadius = m_BulletRadius;
rb.mass = 0.2f;
rb.friction = 0.3f;
rb.restitution = 0.1f;
// Bullet 脚本项:初速/寿命经参数层传入(与序列化同一条路,
// 下一帧 ScriptRuntime 对账实例化)
auto& sc = bullet.AddComponent<Engine::ECS::ScriptComponent>();
Engine::ECS::ScriptComponent::Item item;
item.name = "Bullet";
item.params.push_back(
{"Velocity",
Engine::Scripting::ParamValue{fwd * m_BulletSpeed}});
item.params.push_back(
{"Lifetime", Engine::Scripting::ParamValue{5.0f}});
sc.items.push_back(std::move(item));
}
f32 m_BulletSpeed = 25.0f;
f32 m_MuzzleOffset = 0.6f;
f32 m_FireCooldown = 0.15f;
f32 m_BulletRadius = 0.08f;
f32 m_Cooldown = 0.0f;
bool m_FireWasDown = false; // 左键上升沿检测基准
};
ENGINE_REGISTER_SCRIPT(Bullet)
ENGINE_REGISTER_SCRIPT(ImpactFx)
ENGINE_REGISTER_SCRIPT(TestShoot)
} // namespace
+233
View File
@@ -0,0 +1,233 @@
//
// ThirdPersonCamera.cpp:第三人称环绕相机(UE SpringArm 同位)。
//
// 挂 primary 相机实体。鼠标增量 → yaw/pitch,相机位置 = 目标实体
// (按名解析,默认 "player")+ 球面偏移,朝向锁定目标。WASDMove 的
// 移动参考 = 本相机朝向——WASD 即"相机水平朝向的前后左右"(UE
// gameplay 语义)。枢轴水平硬跟、竖直带阻尼(FollowResponse):XZ
// 与角色零滞后(跑动不错位),Y 滤掉脚底每物理步的微弹跳(踩碎块/
// 撞墙)避免画面抖动。
//
// OnStart 从编辑器摆的机位反推 yaw/pitch/dist:编辑器摆哪,游戏就
// 从哪个角度开场。Esc 释放光标(调参),左键重捕继续玩。
//
// 防穿墙:枢轴→期望机位做静态几何射线(UE SpringArm 同位),命中则拉近;
// 拉近瞬时、拉远平滑,贴墙最小距离可配。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Core/Input.h>
#include <Engine/Core/Log.h>
#include <Engine/ECS/Components.h>
#include <Engine/ECS/Scene.h>
#include <Engine/Physics/PhysicsRay.h> // RayHit(防穿墙射线结果)
#include <Engine/Physics/PhysicsWorld.h> // App().GetPhysics().RayCastStatic 门面
#include <entt/entt.hpp>
#include <glm/glm.hpp>
#include <algorithm>
#include <cmath>
using namespace Engine;
namespace {
// 拉远回弹响应(1/s,指数逼近):离开墙角后镜头平滑归位。
// 拉近是瞬时的(见 OnUpdate)——遮挡晚一帧就会穿墙,不能平滑
constexpr float kCameraReturnResponse = 8.0f;
class ThirdPersonCamera final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "ThirdPersonCamera"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("Sensitivity", &m_Sensitivity, 1.0f);
pb.Field("Distance", &m_Distance, 0.0f); // 0 = 吸编辑器机位
pb.Field("HeightOffset", &m_HeightOffset, 1.2f);
pb.Field("PitchMinDeg", &m_PitchMin, -35.0f);
pb.Field("PitchMaxDeg", &m_PitchMax, 75.0f);
pb.Field("GamepadTurnSpeed", &m_PadTurnSpeed, 180.0f);
// 跟随目标按名锁定(默认 "player"):场景有多个角色控制器
// 实体(如跟随 NPC)时,按名解析保证相机锁主角而非随机命中
pb.Field("TargetName", &m_TargetName, std::string("player"));
// 竖直跟随阻尼(1/s,指数逼近;0 = 硬跟=旧行为)。水平 XZ
// 恒硬跟(保证跑动时相机/角色不错位、无拖影——全轴阻尼会
// 让角色在高速移动时被"甩"出画面并抖动);仅 Y 阻尼吸收
// 角色脚底每物理步的微弹跳(踩碎块/撞墙),帧率无关
pb.Field("FollowResponse", &m_FollowResponse, 12.0f);
// 目标 Y 瞬移超过该距离(PIE 传送/回合复位)→ 直接吸附
pb.Field("SnapDistance", &m_SnapDistance, 3.0f);
// 相机防穿墙(UE SpringArm 同位):枢轴→期望机位之间若有静态
// 几何,把相机拉到命中点前。关掉 = 旧行为(可穿墙)
pb.Field("CameraCollision", &m_Collision, true);
// 贴墙时相机离枢轴的最小距离(再近会切进角色/近平面)
pb.Field("MinDistance", &m_MinDistance, 0.35f);
// 命中点回退余量(防止相机面贴墙 → z-fighting/切面)
pb.Field("CollisionSkin", &m_CollisionSkin, 0.25f);
}
void OnStart() override {
auto& reg = App().GetScene().GetRegistry();
// ---- 找目标:优先按名(TargetName)命中主角 ----
auto byName = reg.view<Engine::ECS::NameComponent>();
for (auto e : byName) {
if (byName.get<Engine::ECS::NameComponent>(e).name ==
m_TargetName) {
m_Target = e;
break;
}
}
// 名称未命中(未命名/改名)→ 回退第一个角色控制器实体
if (m_Target == entt::null) {
auto view = reg.view<Engine::ECS::CharacterControllerComponent>();
if (view.empty()) {
ENGINE_WARN(
"[ThirdPersonCamera] 场景无 CharacterController 实体,相机静止");
return;
}
m_Target = *view.begin();
}
// ---- 从编辑器机位反推 yaw/pitch/dist(摆哪开场在哪)----
const auto& camT = Transform();
const auto& tgtT =
App().GetScene().GetRegistry().get<
Engine::ECS::TransformComponent>(m_Target);
const glm::vec3 offset =
camT.GetPosition() -
(tgtT.GetPosition() +
glm::vec3(0.0f, m_HeightOffset, 0.0f));
const float dist = glm::length(offset);
if (dist > 1e-4f && m_Distance <= 0.0f) {
m_Distance = dist;
m_Pitch =
glm::degrees(std::asin(std::clamp(offset.y / dist,
-1.0f, 1.0f)));
m_Yaw = glm::degrees(std::atan2(offset.x, offset.z));
}
ENGINE_INFO(
"[ThirdPersonCamera] 目标已锁定,dist={:.2f} yaw={:.1f} pitch={:.1f}",
m_Distance, m_Yaw, m_Pitch);
Input::SetCursorCaptured(true);
}
void OnUpdate(float dt) override {
if (m_Target == entt::null) {
return;
}
// 光标管理:Esc 释放(Inspector 调参),左键重捕。
// 手柄玩家不碰鼠标——光标保持捕获态不影响摇杆输入
if (Input::IsKeyDown(Input::Key::Escape)) {
Input::SetCursorCaptured(false);
}
if (Input::IsMouseButtonDown(Input::MouseButton::Left)) {
Input::SetCursorCaptured(true);
}
// ---- 鼠标增量 + 右摇杆 → 球面角 ----
// 鼠标 = 相对增量(GetMouseDelta);右摇杆 = 持续量
//(Look 动作,满推 m_PadTurnSpeed 度/秒),两者叠加
float dx = 0.0f, dy = 0.0f;
Input::GetMouseDelta(dx, dy);
const glm::vec2 padLook = Input::ActionValue2("Look");
const float k = 0.12f * m_Sensitivity;
m_Yaw -= dx * k + padLook.x * m_PadTurnSpeed * dt;
m_Pitch += dy * k + padLook.y * m_PadTurnSpeed * dt;
m_Pitch = std::clamp(m_Pitch, m_PitchMin, m_PitchMax);
m_Yaw = std::fmod(m_Yaw, 360.0f);
// ---- 位置 = 目标枢轴 + 球面偏移 ----
// 水平 XZ 硬跟(与角色零滞后——跑动时相机/角色不错位、无
// 拖影),仅 Y 经阻尼逼近(FollowResponse):吸收目标脚底
// 每物理步的微弹跳(踩碎块/撞墙),相机竖直方向不再 1:1
// 抖动。首帧、瞬移(超 SnapDistance)或阻尼为 0 时直接吸附
// = 旧硬跟语义
const auto& tgtT =
App().GetScene().GetRegistry().get<
Engine::ECS::TransformComponent>(m_Target);
const glm::vec3 rawPivot =
tgtT.GetPosition() +
glm::vec3(0.0f, m_HeightOffset, 0.0f);
if (!m_FollowInit || m_FollowResponse <= 0.0f ||
std::abs(rawPivot.y - m_FollowPivotY) > m_SnapDistance) {
m_FollowPivotY = rawPivot.y;
m_FollowInit = true;
} else {
m_FollowPivotY +=
(rawPivot.y - m_FollowPivotY) *
(1.0f - std::exp(-m_FollowResponse * dt));
}
glm::vec3 pivot = rawPivot; // XZ 硬跟
pivot.y = m_FollowPivotY; // Y 阻尼
const float yawR = glm::radians(m_Yaw);
const float pitchR = glm::radians(m_Pitch);
const glm::vec3 dir( // 枢轴 → 相机(yaw 绕 Y,pitch 抬升)
std::sin(yawR) * std::cos(pitchR),
std::sin(pitchR),
std::cos(yawR) * std::cos(pitchR));
// ---- 相机防穿墙:枢轴 → 期望机位射线拉近 ----
// 只打静态几何(碎块/动态物不当遮挡,避免镜头被飘过的碎片
// 乱拉);枢轴在角色胶囊内,角色是 CharacterVirtual 不进射线,
// 不会自己挡自己。拉近瞬时(遮挡晚一帧就穿墙),拉远平滑
float wantDist = m_Distance;
if (m_Collision) {
RayHit hit;
if (App().GetPhysics().RayCastStatic(
pivot, dir, m_Distance + m_CollisionSkin, hit)) {
wantDist = std::max(hit.distance - m_CollisionSkin,
m_MinDistance);
}
}
if (!m_DistInit) { // 首帧吸附(无回弹动画)
m_CurDist = wantDist;
m_DistInit = true;
} else if (wantDist < m_CurDist) {
m_CurDist = wantDist; // 拉近:瞬时
} else {
m_CurDist += (wantDist - m_CurDist) *
(1.0f - std::exp(-kCameraReturnResponse * dt));
}
auto& camT = Transform();
camT.SetPosition(pivot + dir * m_CurDist);
// ---- 朝向锁定目标 ----
// 引擎 YXZ 序(R = Ry·Rx)视向 = (-cosP sinY, sinP, -cosP cosY)
// 反解:pitch = asin(vd.y),yaw = atan2(-vd.x, -vd.z)
const glm::vec3 vd = glm::normalize(pivot - camT.GetPosition());
const float p = glm::degrees(std::asin(std::clamp(vd.y, -1.0f, 1.0f)));
const float y = glm::degrees(std::atan2(-vd.x, -vd.z));
camT.SetRotation(glm::vec3(p, y, 0.0f));
}
private:
float m_Sensitivity = 1.0f;
float m_Distance = 0.0f; // 0 = OnStart 吸编辑器机位
float m_HeightOffset = 1.2f; // 枢轴高度(瞄胸口)
float m_PitchMin = -35.0f;
float m_PitchMax = 75.0f;
float m_PadTurnSpeed = 180.0f; // 右摇杆满推角速度(度/秒)
std::string m_TargetName = "player"; // 目标实体名(NameComponent)
float m_FollowResponse = 12.0f; // 竖直跟随阻尼 1/s(0 = 硬跟)
float m_SnapDistance = 3.0f; // Y 瞬移吸附阈值 m
float m_FollowPivotY = 0.0f; // 阻尼后的枢轴高度(XZ 硬跟)
bool m_FollowInit = false; // 首帧吸附基准
bool m_Collision = true; // 相机防穿墙开关
float m_MinDistance = 0.35f; // 贴墙最小距离
float m_CollisionSkin = 0.25f; // 命中点回退余量
float m_CurDist = 0.0f; // 防穿墙后的当前距离(拉近平滑态)
bool m_DistInit = false; // 距离首帧吸附基准
float m_Yaw = 0.0f;
float m_Pitch = 20.0f;
entt::entity m_Target = entt::null;
};
ENGINE_REGISTER_SCRIPT(ThirdPersonCamera)
} // namespace
+279
View File
@@ -0,0 +1,279 @@
//
// WASDMove.cpp:第三人称移动(WASD/手柄摇杆,相机朝向参考,UE gameplay 语义)。
//
// 输入走引擎具名动作层(Move/Jump/Run 默认绑定):WASD + 方向键 +
// 手柄左摇杆移动(引擎径向死区),Space/手柄 A 跳;跑 = Run 动作
//(Shift / 手柄 L3 按下左摇杆)。摇杆模拟量直接驱动动量整形(轻推慢走)。
//
// 移动语义分派(按实体配置自动选择,优先级从上到下):
// - CharacterControllerComponent → 角色控制器路径(扫掠运动学:
// 水平喂意图速度 / 重力自动积分 / Space 跳跃,Unity
// CharacterController.Move 同构)
// - Dynamic 刚体 → 走物理速度门面(每帧喂水平速度,Y 归物理——
// 重力/跳跃不受影响;松键喂零速 = 立即停,Unity velocity 语义)
// - 都没有 → 直接平移 Transform
//
// 编辑器形态键位天然不冲突:RMB 按住 = 相机吃 WASD 飞行(脚本也读
// 到按键 → 同时在动,测移动时松开 RMB 即可);游戏形态 WASD 常开。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Application.h>
#include <Engine/Physics/PhysicsWorld.h>
#include <Engine/Core/Input.h>
#include <Engine/Debug/DebugDraw.h>
#include "ScriptDebug.h"
#include "ScriptLog.h"
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/EditorCamera.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cmath>
using namespace Engine; // Input 门面(统一输入入口)
namespace {
class WASDMove final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "WASDMove"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
pb.Field("Speed", &m_Speed, 3.0f); // 走路 m/s
pb.Field("RunSpeed", &m_RunSpeed, 6.0f); // 跑步 m/s
pb.Field("TurnResponse", &m_TurnResponse, 12.0f); // 转向阻尼
pb.Field("AccelResponse", &m_AccelResponse, 8.0f); // 加速/转向响应
pb.Field("StopResponse", &m_StopResponse, 14.0f); // 松手制动响应
pb.Field("JumpSpeed", &m_JumpSpeed, 6.0f); // 起跳初速 m/s
// clip 按名引用(Unity anim.Play("Walk") 语义)——名字在
// Inspector 里改,attach 顺序变了脚本零改动
pb.Field("IdleClip", &m_IdleClip, std::string("idle"));
pb.Field("WalkClip", &m_WalkClip, std::string("walk"));
pb.Field("RunClip", &m_RunClip, std::string("run"));
}
void OnStart() override {
SLOG_TRACE("hello");
SLOG_INFO("hello");
SLOG_DEBUG("hello");
SLOG_WARN("hello");
SLOG_ERROR("hello");
SLOG_INFO("[WASDMove] 就绪:走 {:.1f} m/s / 跑 {:.1f} m/s / 跳 {:.1f} m/s",
m_Speed, m_RunSpeed, m_JumpSpeed);
}
void OnUpdate(float dt) override {
// 位移方向 = 视线水平投影(W 前进 / A 左 / D 右),UE Scene
// view 移动惯例;Y 锁 0(升降/跳跃归物理或将来扩展)。
// 输入 = 具名动作层(Unity6 Input System / UE5 Enhanced Input
// 同位):Move 默认混绑 WASD/方向键/左摇杆(引擎做径向死区 +
// 单位圆钳制,斜向不超速),Jump 混绑 Space/手柄 A——键盘
// 手柄一套代码,将来改键/加绑定不进脚本
// 移动参考 = 场景 primary 相机(第三人称相机脚本每帧写它
// 的朝向——玩家看到的视角就是移动参考);无相机实体时
//(纯独立形态)回退编辑器相机
glm::quat cq;
bool refCam = false;
auto camView = App().GetScene()
.GetRegistry()
.view<Engine::ECS::CameraComponent>();
for (auto e : camView) {
const auto& cam =
camView.get<Engine::ECS::CameraComponent>(e);
if (cam.primary && cam.active) {
cq = App().GetScene()
.GetRegistry()
.get<Engine::ECS::TransformComponent>(e)
.GetRotationQuat();
refCam = true;
break;
}
}
if (!refCam) {
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);
// 约定:X+ = 右(D),Y- = 前(W);方向合成 = fwd*(-Y) + right*(+X)
const glm::vec2 mvRaw = Input::ActionValue2("Move");
glm::vec2 mv = mvRaw;
if (glm::length(mv) > 1.0f) mv = glm::normalize(mv);
glm::vec3 move = fwd * (-mv.y) + right * mv.x;
move.y = 0.0f;
// 只钳超速,不放大——摇杆模拟量(幅度 <1)保留进速度:
// 轻推慢走、推满全速(归一化会把幅度吃掉,一动就满速)
if (glm::length(move) > 1.0f) {
move = glm::normalize(move);
}
// 走/跑:Run 动作(默认绑 Shift + 手柄 L3,按下左摇杆冲刺
// 的 Xbox 惯例)。不判"摇杆推满"——键盘 WASD 幅度恒 1.0,
// 那样判会把走路误判成跑
const bool running = Input::ActionDown("Run");
const float speed = running ? m_RunSpeed : m_Speed;
const glm::vec3 desired = move * speed; // 期望速度(输入意图)
auto& phy = App().GetPhysics();
const bool hasCC =
TryGetComponent<Engine::ECS::CharacterControllerComponent>() !=
nullptr;
const bool grounded =
hasCC ? phy.IsCharacterGrounded(EntityBits()) : true;
// ---- 动量整形(惯性的本体,纯脚本):真实速度逐帧逼近期
// 望速度——起步渐快、转向沿弧线漂移、折返经零减速再反向、
// 松手滑行刹车。加速/转向用 AccelResponse,松手用更硬的
// StopResponse(角色要"想停就停");空中操纵性降到 1/4
//(起跳后不宜急转弯)。指数逼近,帧率无关
const bool moving = glm::length(mvRaw) > 0.1f;
float response = moving ? m_AccelResponse : m_StopResponse;
if (!grounded) {
response *= 0.25f;
}
m_PlanarVel +=
(desired - m_PlanarVel) * (1.0f - std::exp(-response * dt));
if (glm::length(desired - m_PlanarVel) < 0.02f) {
m_PlanarVel = desired; // 贴齐,防低速蠕动
}
// ---- 路径 1:角色控制器(挂了 CharacterControllerComponent)----
// 水平喂动量整形后的真实速度;Y 保留现值——重力由控制器每
// 步自动积分,每帧覆写 Y 会把下落速度清零(空中变悬浮)
if (hasCC) {
const glm::vec3 cur = phy.GetCharacterVelocity(EntityBits());
phy.SetCharacterVelocity(
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
// 跳跃:动作层边沿(Pressed = prev 0 → cur 非 0,键盘
// Space / 手柄 A 都产生一次上升沿)。IsGrounded 防空中连跳
if (Input::ActionPressed("Jump") &&
phy.IsCharacterGrounded(EntityBits())) {
phy.Jump(EntityBits(), m_JumpSpeed);
SLOG_INFO("[WASDMove] jump(着地起跳 {:.1f} m/s)",
m_JumpSpeed);
}
} else if (auto* rb =
TryGetComponent<Engine::ECS::RigidBodyComponent>();
rb != nullptr && rb->bodyValid &&
rb->type == Engine::ECS::RigidBodyComponent::BodyType::Dynamic) {
// 物理驱动:只覆写水平速度,Y 保留物理积分离散结果
//(整体喂 0 会抵消重力 → 空中物体悬浮)
const glm::vec3 cur = phy.GetLinearVelocity(EntityBits());
phy.SetLinearVelocity(
EntityBits(), {m_PlanarVel.x, cur.y, m_PlanarVel.z});
} else {
Transform().SetPosition(Transform().GetPosition() +
m_PlanarVel * dt);
}
// ---- 朝向:面向"真实速度"方向(不是输入方向,模型默认 +Z)----
// 这是消"平移感"的关键:动量整形后速度滞后于输入,若面朝
// 输入方向,身体朝前脚却在侧滑——正是割裂感的来源。面朝
// 真实速度 = 脚底朝向与位移一致,转向呈现弧线漂移。速度
// 低于阈值(起步瞬间/折返过零)锁定上帧朝向:折返表现成
// U 弯回转而不是原地瞬转。只取水平面(角色不俯仰翻滚)
if (glm::length(m_PlanarVel) > 0.3f) {
const float targetYaw =
std::atan2(m_PlanarVel.x, m_PlanarVel.z);
const glm::vec3 modelFwd =
Transform().GetRotationQuat() * glm::vec3(0.0f, 0.0f, 1.0f);
const float curYaw = std::atan2(modelFwd.x, modelFwd.z);
const float dyaw = std::atan2(std::sin(targetYaw - curYaw),
std::cos(targetYaw - curYaw));
const float k = 1.0f - std::exp(-m_TurnResponse * dt);
Transform().SetRotationQuat(
glm::angleAxis(curYaw + dyaw * k, glm::vec3(0, 1, 0)));
}
// ---- 动画:按真实速度分档(迟滞防临界抖)----
// 判据 = 动量整形后的真实水平速度,不是输入原始值:起步
// 滑行期已是 walk,松手刹车期保持 walk 直到真停——动画与
// 位移观感一致,按键瞬通瞬断不再瞬间换挡。run 双阈值迟滞
//(进 0.9×RunSpeed / 退 0.7×RunSpeed):摇杆在阈值附近晃
// 不抖档。引擎 Play(名) 自带相位对齐 + crossfade,切档
// 突兀感只剩判据本身的问题
const float planarSpeed = glm::length(m_PlanarVel);
std::string target;
if (planarSpeed < 0.15f) {
target = m_IdleClip;
} else if (m_CurClip == m_RunClip) {
target = (planarSpeed >= m_RunSpeed * 0.7f) ? m_RunClip
: m_WalkClip;
} else {
target = (planarSpeed >= m_RunSpeed * 0.9f) ? m_RunClip
: m_WalkClip;
}
if (target != m_CurClip) {
if (auto* anim = TryGetComponent<Engine::ECS::AnimationComponent>();
anim != nullptr && anim->Play(target, 0.35f)) {
// fade 0.35s:locomotion 混合的经验值(Unity 默认
// 0.25 太短,0.5+ 显拖沓);相位同步在引擎侧做
m_CurClip = target; // 播放成功才记账(模型未就绪下帧重试)
}
}
const glm::vec3 anchor =
(hasCC ? phy.GetCharacterPosition(EntityBits())
: Transform().GetPosition()) +
glm::vec3(0.0f, 0.05f, 0.0f);
// ---- 运动线(ScriptDbg 封装:像素线宽 3px / 当帧瞬时 /
// 透视直显已固化为默认值,不用记参数序):蓝 = 输入期
// 望速度,绿 = 动量整形后的真实速度——同一起点,夹角
// 即惯性滞量(转向弧线/U 弯/刹车可视化)
ScriptDbg::Arrow(anchor, anchor + desired * 0.2f, 0x2B5FD0);
ScriptDbg::Arrow(anchor, anchor + m_PlanarVel * 0.2f, 0x1E9E42);
// ---- 力学线:重力(红,下)+ 接触反力(黄,上,仅着地)。
// 长度 ∝ gravityScale:静置时红黄等长 = 力平衡;起跳
// 离地黄箭头消失 = 离地即失支撑;gravityScale 调大两
// 箭同步变长(重量感可视化)
float gs = 1.0f;
if (auto* cc = TryGetComponent<
Engine::ECS::CharacterControllerComponent>()) {
gs = cc->gravityScale;
}
const float gLen = 0.5f * gs;
ScriptDbg::Gravity(anchor, gLen);
ScriptDbg::Support(anchor, grounded ? gLen : 0.0f);
const glm::vec3 vel = hasCC
? phy.GetCharacterVelocity(EntityBits())
: m_PlanarVel;
if (dt > 1e-5f) {
m_AccelS = glm::mix(m_AccelS, (vel - m_PrevVel) / dt, 0.35f);
}
m_PrevVel = vel;
ScriptDbg::Accel(anchor, m_AccelS);
}
void DebugShow() {
}
private:
float m_Speed = 3.0f; // 走路 m/s(声明默认;存档值覆盖)
float m_RunSpeed = 6.0f; // 跑步 m/s(Shift 按住)
float m_TurnResponse = 12.0f; // 转向阻尼强度(越大越跟手)
float m_AccelResponse = 8.0f; // 加速/转向响应(动量整形,越小越"重")
float m_StopResponse = 14.0f; // 松手制动响应(越大刹车越硬)
glm::vec3 m_PlanarVel{0.0f}; // 平滑后的水平真实速度(动量状态)
float m_JumpSpeed = 6.0f; // 起跳初速(配合组件 gravityScale 调手感)
std::string m_IdleClip = "idle"; // clip 名(Inspector 可改)
std::string m_WalkClip = "walk";
std::string m_RunClip = "run";
std::string m_CurClip = "idle"; // 当前目标 clip(边沿检测基准)
glm::vec3 m_PrevVel{0.0f}; // 上帧测量速度(加速度差分基准)
glm::vec3 m_AccelS{0.0f}; // 平滑测量加速度(EMA 0.35,调试图)
};
ENGINE_REGISTER_SCRIPT(WASDMove)
} // namespace
+87
View File
@@ -0,0 +1,87 @@
//
// WallFracture.cpp:可碎墙脚本——碎裂逻辑已提取至 FractureLib.h
//(打靶场 Target 复用同一条路径),本文件只剩宿主职责:
// DeclareParams 直绑 Burst::p 各字段(参数名/默认值与历史档位
// 逐一同名同值,旧场景不受影响)
// OnCollisionEnter → Burst::TryFire(一次性保护 + 对方撞击来源
// 与速度校验 + 藏墙 + Voronoi 切分 + spawn 碎片群,库内完成)。
// 撞击来源 = 动态刚体(子弹/抛体)或角色(玩家撞墙)——角色是
// CharacterVirtual,其碰撞事件由 PhysicsWorld 从角色接触合成
// (Jolt 的 ContactListener 只回调刚体,不覆盖角色)
// OnUpdate → Burst::Tick(施速待办 / 寿命清理 / 模型回收)
// OnDestroy → Burst::Shutdown(独占碎片模型批量停飞卸载)
//
// 外观默认继承墙的生效材质(底色/贴图/透明),碎块与墙面观感一致;
// 要独立外观(玻璃/发光碎片)置 Burst::look.inheritFromHost = false
// 再用 baseColor/emissive 显式指定。
//
// 验收步骤:编辑器摆一堵墙(MeshRenderer = procedural://cube +
// RigidBody Static Box + 挂 WallFracture,缩放即墙尺寸)→ 场景里
// 任意实体挂 TestShoot → Play → RMB 飞行瞄准墙体 → 左键射击:
// 墙沿裂纹碎成不规则碎片,孔洞周边碎块小、飞散明显,边缘大块
// 塌落堆积在墙脚,若干秒后清空。
//
#include <Engine/Scripting/Script.h>
#include <Engine/Core/Types.h>
#include "FractureLib.h"
using namespace Engine; // u32/f32/i32 基础类型(历史脚本同款)
namespace {
// ---- 可碎墙(挂墙实体;触发后由 Burst 管一整批碎片)----
class WallFracture final : public Engine::Scripting::ScriptBehavior {
public:
const char* Name() const override { return "WallFracture"; }
void DeclareParams(Engine::Scripting::ParamBuilder& pb) override {
// Voronoi 种子数 = 碎片数(40% 在命中点半径内加密 → 孔洞
// 周边碎块小、边缘大块的玻璃碎分布;最小间距防碎片退化)
// 直绑 Burst::p:碎裂逻辑只读 Burst::p,绑到别处(曾经的
// 独立 m_P)会让计数器/寿命等全部失效
pb.Field("Count", &m_Burst.p.count, 30);
pb.Field("SeedRadius", &m_Burst.p.seedRadius, 0.9f); // 加密半径 m
pb.Field("MinSeedDist", &m_Burst.p.minSeedDist, 0.12f); // 种子最小间距 m
// 玻璃/陶瓷式碎裂(裂纹分离 + 重力塌落),非爆炸飞散:
// 初速 = 命中点径向 × 距离指数衰减(近动远不动)+ 法线
// 轻微外推(剥落)+ 微弱上抛;重力是塌落的主导
pb.Field("BurstSpeed", &m_Burst.p.burstSpeed, 1.2f); // 近点径向初速 m/s
pb.Field("Falloff", &m_Burst.p.falloff, 1.0f); // 距离衰减率 1/m
pb.Field("NormalPop", &m_Burst.p.normalPop, 0.4f); // 法线外推 m/s
pb.Field("UpBias", &m_Burst.p.upBias, 0.3f); // 上抛偏置 m/s
pb.Field("Lifetime", &m_Burst.p.lifetime, 6.0f); // 碎片寿命(秒)
pb.Field("MaxFragments", &m_Burst.p.maxFragments, 128); // 活碎片上限
pb.Field("MinImpactSpeed", &m_Burst.p.minImpactSpeed, 2.0f); // 触发阈值
}
void OnCollisionEnter(u32 other, const glm::vec3& point,
const glm::vec3& normal) override {
// 触发条件在库内:对方须是够快的撞击来源——动态刚体
//(子弹/抛体)或角色(玩家撞墙);速度阈值过滤静止贴靠
m_Burst.TryFire(HV(), other, point, normal);
}
void OnUpdate(f32 dt) override { m_Burst.Tick(HV(), dt); }
// 墙销毁(PIE stop 换血 / 手动删除):碎片实体由场景清亡处理,
// 独占碎片模型统一批量回收
void OnDestroy() override { m_Burst.Shutdown(HV()); }
private:
// 库桥接:本成员函数内可访问 protected 基类设施(App/Spawn/
// Destroy),库内只走公共 API——见 FractureLib.h HostView 注
Engine::Fracture::HostView HV() {
return {App(), EntityBits(),
[this](const std::string& n) { return Spawn(n); },
[this](Engine::ECS::Entity e) { Destroy(e); }};
}
// 碎片簿记 + 生命周期;参数即 m_Burst.p(DeclareParams 直绑其字段)
Engine::Fracture::Burst m_Burst;
};
} // namespace
ENGINE_REGISTER_SCRIPT(WallFracture)
+44 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.20)
project(HelloVulkan CXX)
project(HelloVulkan C CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@@ -12,6 +12,31 @@ add_subdirectory(Lib/glfw)
add_subdirectory(Lib/glm)
add_subdirectory(Lib/spdlog)
add_subdirectory(Lib/stb)
# meshoptimizer(submodule,LOD 网格简化;demo/gltfpack 默认关闭,install 不需要)
set(MESHOPT_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(Lib/meshoptimizer)
# ---- Jolt Physics(submodule v5.6,刚体/软体/角色/载具模拟)----
if(MSVC)
set(USE_STATIC_MSVC_RUNTIME_LIBRARY OFF CACHE BOOL "" FORCE)
endif ()
# 顶层 CMake 在 Build/ 子目录;只编 jolt 静态库,示例/测试目标默认全关
set(JPH_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE) # LTO 拖慢开发迭代
set(OVERRIDE_CXX_FLAGS OFF CACHE BOOL "" FORCE) # 不动全局优化参数
set(FLOATING_POINT_EXCEPTIONS_ENABLED OFF CACHE BOOL "" FORCE) # NaN 进物理不炸,仅报警
# JPH_USE_CPU_COMPUTE 默认 ON,需要 DXC 把 Shaders/*.hlsl 编成 .spv(本机无
# 工具链);我们不使用 Jolt 的 GPU compute 路径,连同后端 shader 一并关掉
set(JPH_USE_CPU_COMPUTE OFF CACHE BOOL "" FORCE)
set(JPH_USE_DX12 OFF CACHE BOOL "" FORCE)
set(JPH_USE_VK OFF CACHE BOOL "" FORCE)
set(JPH_USE_MTL OFF CACHE BOOL "" FORCE)
# Jolt GENERATE_DEBUG_SYMBOLS 默认 ON,Release 也带 -g → Jolt 的 DWARF
# Debug/RelWithDebInfo 保留符号便于调试
if(CMAKE_BUILD_TYPE STREQUAL "Release")
set(GENERATE_DEBUG_SYMBOLS OFF CACHE BOOL "" FORCE)
endif()
add_subdirectory(Lib/JoltPhysics/Build)
# ---- Assimp(vendored submodule,模型导入;只编译需要的 importer) ----
set(BUILD_SHARED_LIBS OFF) # 静态链接
@@ -39,11 +64,29 @@ set(ASSIMP_BUILD_STL_IMPORTER ON CACHE BOOL "" FORCE)
set(ASSIMP_BUILD_PLY_IMPORTER ON CACHE BOOL "" FORCE)
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
find_package(Vulkan REQUIRED)
# ---- editor/game 编译期分离(UE WITH_EDITOR / Unity UNITY_EDITOR 模式)----
# ON(默认):HELLO_EDITOR=1,材质球预览 tile 池 + App/editor 编辑器 UI 全量编译
# OFF:game 构建——预览 pass/atlas/请求池、previewEnvSets、App/editor 源码
# 整体不进编译产物(宏裁剪,非运行时判断)
option(HELLOVULKAN_EDITOR "Build editor modules (material preview, inspector UI)" ON)
if(MINGW)
set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -Wl,-Bdynamic -lucrt")
endif()
# Engine 和 App
add_subdirectory(Engine)
add_subdirectory(App)
+79 -11
View File
@@ -1,36 +1,104 @@
file(GLOB_RECURSE ENGINE_SOURCES
src/*.cpp
)
# ============ 第三方库目标(Engine 的依赖) ============
# ImGui(docking 分支,vendored submodule)
set(IMGUI_DIR ${CMAKE_SOURCE_DIR}/Lib/imgui)
set(IMGUI_SOURCES
add_library(imgui STATIC
${IMGUI_DIR}/imgui.cpp
${IMGUI_DIR}/imgui_draw.cpp
${IMGUI_DIR}/imgui_tables.cpp
${IMGUI_DIR}/imgui_widgets.cpp
# ${IMGUI_DIR}/imgui_demo.cpp # ShowDemoWindow 的实现(调试期保留)
${IMGUI_DIR}/backends/imgui_impl_glfw.cpp
${IMGUI_DIR}/backends/imgui_impl_vulkan.cpp
)
target_include_directories(imgui PUBLIC
${IMGUI_DIR}
${IMGUI_DIR}/backends)
# 两个 backend 头文件分别依赖 GLFW / Vulkan 头
target_link_libraries(imgui PUBLIC glfw Vulkan::Vulkan)
# 编译成静态库
add_library(Engine STATIC ${ENGINE_SOURCES} ${IMGUI_SOURCES})
# ImGuizmo(视口操纵杆,vendored submodule;新版仓库源码在 src/,
# 只编译 ImGuizmo.cpp,GraphEditor/ImCurveEdit 等示例模块不用)
# IMGUI_DEFINE_MATH_OPERATORS 必须 PUBLIC:ImGuizmo.cpp 与任何
# include 其头文件的 TU 都要在 imgui.h 之前看到该宏(ImVec2 运算符)
set(IMGUIZMO_DIR ${CMAKE_SOURCE_DIR}/Lib/ImGuizmo/src)
add_library(ImGuizmo STATIC ${IMGUIZMO_DIR}/ImGuizmo.cpp)
target_include_directories(ImGuizmo PUBLIC ${IMGUIZMO_DIR})
target_compile_definitions(ImGuizmo PUBLIC IMGUI_DEFINE_MATH_OPERATORS)
target_link_libraries(ImGuizmo PUBLIC imgui)
# ufbx(FBX 全版本导入,vendored 单文件 C 库):assimp 的 FBX importer
# 只认 FBX 2011+,Mixamo 及大量网上资产是 FBX 6.1 binary(二进制布局
# 与 7.x 在对象 ID 层不兼容,assimp DOM 直接拒读)。ufbx 官方支持
# FBX 5.x/6.x/7.x 全版本,引擎的 .fbx 解析统一走它(glTF/OBJ 仍 assimp)
add_library(ufbx STATIC ${CMAKE_SOURCE_DIR}/Lib/ufbx/ufbx.c)
target_include_directories(ufbx PUBLIC ${CMAKE_SOURCE_DIR}/Lib/ufbx)
# Tracy(CPU/GPU 帧分析)
add_library(tracy STATIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public/TracyClient.cpp)
target_include_directories(tracy PUBLIC ${CMAKE_SOURCE_DIR}/Lib/tracy/public)
target_compile_definitions(tracy PUBLIC
TRACY_ENABLE
# 按需剖析:GPU context 宣告(GpuNewContext)与连接绑定——GUI 永远晚于
# 程序启动连接,没有这个宏宣告消息被丢弃,GPU 时间线永远空白
TRACY_ON_DEMAND)
find_package(Threads REQUIRED)
if(WIN32)
target_link_libraries(tracy PUBLIC
Threads::Threads
ws2_32
dbghelp
secur32)
else()
target_link_libraries(tracy PUBLIC Threads::Threads)
endif()
# ============ Engine ============
file(GLOB_RECURSE ENGINE_SOURCES CONFIGURE_DEPENDS src/*.cpp)
# game 构建(HELLOVULKAN_EDITOR=OFF):editor 专属 pass 整 TU 不进产物
#(材质球预览 / Editor gizmo——对应头文件只含声明,无符号产出)
if(NOT HELLOVULKAN_EDITOR)
list(REMOVE_ITEM ENGINE_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/Renderer/Passes/MaterialPreviewPass.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Renderer/Passes/EditorGizmoPass.cpp)
endif()
add_library(Engine STATIC ${ENGINE_SOURCES})
# PhysicsDebugDraw 派生 Jolt::DebugRendererSimple:Jolt 以 -fno-rtti 编译
#(有 key function 的基类 typeinfo 符号不存在),该 TU 必须同步关闭 RTTI,
# 否则派生类 typeinfo 引用悬空符号(链接期 undefined reference)
set_source_files_properties(src/Physics/PhysicsDebugDraw.cpp PROPERTIES
COMPILE_OPTIONS "-fno-rtti")
target_include_directories(Engine PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
# header-only 库:纯 include 路径
${CMAKE_SOURCE_DIR}/Lib/entt/src
${IMGUI_DIR}
${IMGUI_DIR}/backends
)
${CMAKE_SOURCE_DIR}/Lib/VulkanMemoryAllocator/include)
# 链接第三方库
target_link_libraries(Engine PUBLIC
glfw
glm::glm
Vulkan::Vulkan
spdlog::spdlog
stb
assimp::assimp
)
ufbx
meshoptimizer
Jolt::Jolt
yaml-cpp::yaml-cpp
imgui
ImGuizmo
tracy)
target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
# editor/game 编译期分离:HELLO_EDITOR=1 时预览子系统 + 编辑器消费端编译
#(PUBLIC——App 层 main.cpp 的编辑器装配同样按此宏裁剪)
if(HELLOVULKAN_EDITOR)
target_compile_definitions(Engine PUBLIC HELLO_EDITOR=1)
endif()
@@ -0,0 +1,70 @@
//
// AnimationAttach:外部动画资产 → 目标模型并入(编辑器 attach 语义,
// 对应 UE "给 SkeletalMesh 指派 AnimSequence" 的跨资产绑定)。
//
// 资产语义:纯动画 FBX(只有骨骼动画无网格)= 引擎的 AnimSequence
// 等价物。轨道映射两级:① 骨骼名同名直映(同源拆分资产零损耗);
// ② 名字不同源时走 HumanoidRig 槽位重定向(Unity Avatar 语义,
// 对每个映射骨骼烘焙 rest→anim 的旋转增量到目标骨架局部空间)。
// 两级都落不上的轨道跳过并计数。
//
// 并入只改运行时内存状态:持久化走场景里 Animation.attachedAnimPaths
//(重启后按列表幂等重放 attach),模型资产本身永不被回写。
//
#ifndef HELLOVULKAN_ANIMATIONATTACH_H
#define HELLOVULKAN_ANIMATIONATTACH_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Model.h>
#include <string>
#include <vector>
namespace Engine {
class Model;
struct ParsedModel;
namespace Animation {
// 重定向明细行(调试:哪个源骨骼经哪个槽位映射到哪个目标骨骼)。
// slot 为静态字符串(HumanoidSlotName 表),名字拷贝
struct RetargetRow {
const char* slot = nullptr;
std::string srcName;
std::string dstName;
};
// attach 结果遥测(Inspector "匹配/跳过" 显示用;
// 幂等命中/解析失败 = 全 0)
struct AttachResult {
u32 clipsAdded = 0;
u32 channelsMatched = 0; // 同名直映轨道数
u32 channelsRetargeted = 0; // 槽位重定向轨道数
u32 channelsSkipped = 0;
std::vector<RetargetRow> retargetRows; // 重定向明细(调试)
};
// 把 src 的 clips 按目标模型 node 表重映射后追加。
// 幂等:sourcePath 已并入过(自愈重放/重复 attach)→ 零操作。
// src 数据被消费(move 语义);无 GPU 依赖,纯 CPU
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
const std::string& sourcePath);
// 纯算法核心(headless 探针可直测):轨道重映射进 dstNodes
// 坐标系,产出的 clips 追加到 outExtra。幂等/命名冲突检查
// 基于 existingClips。AttachClipsToModel 只是
// "取 nodes/clips + 回填" 的薄封装
AttachResult AttachClipsToNodes(
const std::vector<ModelNode>& dstNodes,
const std::vector<AnimationClip>& existingClips,
ParsedModel&& src, const std::string& sourcePath,
std::vector<AnimationClip>& outExtra);
} // namespace Animation
} // namespace Engine
#endif
@@ -0,0 +1,146 @@
//
// AnimationController:动画状态机资产(.animctrl,YAML)——Unity
// AnimatorController / UE AnimBlueprint 数据侧的对应物。
//
// 职责:纯数据资产(参数声明 / 状态表 / 混合树 / 过渡规则),零求值
// 逻辑零运行时状态。clip 按"名字"引用(不持骨骼数据)——同一
// Controller 可复用到任何 clip 名匹配的模型(缺 clip 降级跳过该层,
// 不崩)。加载走 AssetManager 缓存(模式同 MaterialAsset)。
//
// 与求值的接缝:AnimationSystem 每帧把"当前状态 + 组件参数值"翻译成
// PoseLayer[](本资产只提供规则查询),N 层混合在 AnimationEvaluator。
//
#ifndef HELLOVULKAN_ANIMATIONCONTROLLER_H
#define HELLOVULKAN_ANIMATIONCONTROLLER_H
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
#include <string>
#include <unordered_map>
#include <vector>
namespace Engine::Animation {
// 混合树子节点:clip 名 + 2D 平面定位点
struct BlendTreeChild {
std::string clip; // clip 名(运行时按名查 Model::GetClips)
glm::vec2 pos{0.0f}; // 2D 平面定位(1D 时 x=阈值,y 忽略)
};
// 状态的运动源:单 clip 直采 or 混合树
struct BlendTree {
enum class Type : u8 {
Single = 0, // 直接播一个 clip
Directional1D = 1,// 一轴阈值插值(Speed 站走跑)
Directional2D = 2,// 2D 方向混合(MoveX/MoveZ → 8 方向)
};
Type type = Type::Single;
std::string clip; // Single:clip 名
std::string xParam; // 1D/2D:横轴参数名
std::string zParam; // 2D:纵轴参数名
std::vector<BlendTreeChild> children;
};
// 过渡条件(Load 期解析 cond 字符串,运行时只做数值比较)
struct TransitionCond {
enum class Op : u8 { Gt, Lt, Ge, Le, Eq, Ne };
std::string param;
Op op = Op::Gt;
f32 numOperand = 0.0f; // float 比较
bool boolOperand = false; // bool 比较(true/false)
bool isBool = false; // bool 语义(含 trigger 名命中即真)
bool always = false; // cond 空 = 恒真(无条件出边)
bool valid = false; // 解析失败 = false(永不触发)
};
struct Transition {
std::string from; // "*" = AnyState
std::string to;
TransitionCond cond;
f32 fade = 0.25f; // 过渡时长(秒)
bool hasExitTime = false; // true = 当前状态非循环播完一轮才评估
};
struct ControllerState {
std::string name;
BlendTree tree;
f32 speed = 1.0f; // 状态内播放速度倍率(乘组件 speed)
bool loop = true; // false = 播完一轮即停(transitionsOnEnd 生效)
std::vector<std::string> transitionsOnEnd; // 播完出口(按序首个可用)
};
// 参数声明(类型 + 默认值;实例值在 AnimationComponent)
struct ControllerParam {
enum class Type : u8 { Float = 0, Bool = 1, Trigger = 2 };
std::string name;
Type type = Type::Float;
f32 defFloat = 0.0f;
bool defBool = false;
};
// 2D 方向混合树权重求解(单一事实源:System 运行时采样与编辑器
// 方向盘可视化共用同一套数学)——原点子(|pos|<ε)权重 = 1-|v|,
// 方向子找覆盖 v 角度的相邻扇区做角度插值 ×min(|v|,1)。只回有权重
// >0 的子节点(下标入 children + 权重)。依赖 children 已按 Load
// 期角度排序
void EvaluateBlendTree2D(const BlendTree& tree, f32 x, f32 z,
std::vector<std::pair<u32, f32>>& outWeights);
class AnimationController {
public:
// .animctrl YAML 解析;失败返回 nullptr + ERROR 日志。
// Version 缺省键向后兼容(同 MaterialAsset 惯例)
static Scope<AnimationController> Load(const std::string& path);
// 写回 .animctrl(编辑器 Save 用:与 Load 对齐的 YAML 反序列化;
// 失败返回 false + ERROR 日志,缓存实例不动)
bool Save(const std::string& path) const;
// 条件字符串 <-> 编译结构(面板编辑 cond 输入框 Enter 时就地
// 重编译,Save 时反推字符串)
static TransitionCond ParseCond(const std::string& s);
static std::string CondToString(const TransitionCond& c);
const std::vector<ControllerParam>& GetParams() const
{
return m_Params;
}
const std::unordered_map<std::string, ControllerState>&
GetStates() const { return m_States; }
// 状态声明序(编辑器 UI 稳定枚举 + entry 兜底;unordered_map
// 迭代无序,UI/初始化一律走这里)
const std::vector<std::string>& GetStateOrder() const
{
return m_StateOrder;
}
// 入口状态:显式 Entry 键优先,缺省 = states 声明序首个
const std::string& GetEntryState() const { return m_Entry; }
const std::vector<Transition>& GetTransitions() const
{
return m_Transitions;
}
const std::string& GetPath() const { return m_Path; }
// ---- 编辑器可变访问(ControllerEditorPanel 专用;改完
// Save 写回 + AssetManager::ReloadController 刷新缓存)----
std::vector<ControllerParam>& ParamsMut() { return m_Params; }
std::unordered_map<std::string, ControllerState>& StatesMut()
{
return m_States;
}
std::vector<std::string>& StateOrderMut() { return m_StateOrder; }
std::vector<Transition>& TransitionsMut() { return m_Transitions; }
void SetEntry(const std::string& e) { m_Entry = e; }
private:
std::vector<ControllerParam> m_Params;
std::unordered_map<std::string, ControllerState> m_States;
std::vector<std::string> m_StateOrder;
std::vector<Transition> m_Transitions;
std::string m_Entry;
std::string m_Path;
};
} // namespace Engine::Animation
#endif
@@ -0,0 +1,101 @@
//
// AnimationEvaluator:骨骼动画姿态求值器(UE FAnimInstance 单节点求值 /
// Unity PlayableGraph 求值语义的极简对应物)。
//
// 动画系统职责分层(2026-10-06 重构,对齐 UE/Unity):
// Model = 资产层:node/bone/clip 静态数据(assimp 解析产物)
// AnimationComponent = 状态层:播放头/播放设置/crossfade 计时(纯数据)
// AnimationEvaluator = 求值层:clip 时间 → 全节点 global 矩阵(本文件)
// AnimationSystem = 调度层:逐实体推进状态 + 调求值器 + 产 palette
// SceneRenderer = 消费层:只读 palette/entitySkinBases 填实例
//
// 求值器无调度、无渲染依赖:同一姿态可在任意时刻重复求值(渲染主循环
// 与编辑器骨骼可视化复用同一套数学,保证"所见即所渲")。
//
#ifndef HELLOVULKAN_ANIMATIONEVALUATOR_H
#define HELLOVULKAN_ANIMATIONEVALUATOR_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Model.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <vector>
namespace Engine::Animation {
// 单层采样的全节点局部 TRS(Layer 语义)。
// touched = 被任意轨道触碰的节点标记(ComposeGlobals 决定重建或
// 原样——FBX PreRotation 折在 localTransform 里,拆 TRS 重建会丢,
// 未触碰节点必须保持原矩阵)。容量跨帧复用,稳态零堆分配
struct LocalPose {
std::vector<glm::vec3> pos;
std::vector<glm::quat> rot;
std::vector<glm::vec3> scl;
std::vector<u8> touched;
};
// crossfade 求值快照(纯数据):src = 淡出中的来源 clip
//(nullptr = 无过渡 / 过渡已结束),wCur = 当前 clip 权重 0..1
//(smoothstep 已算好)。计时推进与 fadeFrom 状态翻转归 AnimationSystem
struct CrossfadeState {
const AnimationClip* src = nullptr;
f32 srcTime = 0.0f;
f32 wCur = 1.0f;
};
// 单个混合层:一个 clip 采样实例 + 权重。权重语义 = 归一化前的
// 相对权重(求值器内部按有效层权重和归一化;全部为 0 = rest)
struct PoseLayer {
const AnimationClip* clip = nullptr; // nullptr = 该层跳过
f32 time = 0.0f;
f32 weight = 1.0f;
};
// 姿态求值器。内部持有 N 层 LocalPose scratch(跨帧复用容量),
// 单实例非线程安全——动画推进收敛在 AnimationSystem 单点调用
class AnimationEvaluator {
public:
// cur = 当前 clip,curTime = 当前播放头(秒)。
// xf.src 非空时双层局部 TRS 混合(位置/缩放 lerp、旋转 slerp,
// 权重 = xf.wCur)→ global 累乘。
// outGlobals 尺寸 = model.GetNodes().size(),父序 DFS 累乘。
// (crossfade 双层 = EvaluateLayers 的 count==2 特例,同一套
// 加权混合数学,消掉历史双份代码)
// inPlace = 丢弃动画子树根节点的位移(Unity in-place 语义:
// 根运动开关关闭,原地播放,旋转保留)
void Evaluate(const Model& model, const AnimationClip& cur,
f32 curTime, const CrossfadeState& xf,
std::vector<glm::mat4>& outGlobals,
bool inPlace = false);
// N 层加权混合(BlendTree / 状态机过渡的通用入口):
// 逐层采样局部 TRS → 权重归一化混合(pos/scl lerp、rot 渐进
// slerp)→ global 累乘。全空/全零权重 = rest 姿态
void EvaluateLayers(const Model& model, const PoseLayer* layers,
u32 count, std::vector<glm::mat4>& outGlobals,
bool inPlace = false);
// 绑定姿势:全节点 untouched = localTransform 原样累乘。
// 与渲染 identity palette 的表现一致(无动画/组件禁用实体的
// 姿态)——骨骼可视化对非动画实体用它,保证可视化 = 渲染
void ComputeRestPose(const Model& model,
std::vector<glm::mat4>& outGlobals);
private:
// 混合循环的(pose, weight)配对视图:m_Layers 是采样存储,
// m_Sampled 指向其中的有效层并携带原权重
struct SampledLayer {
const LocalPose* pose;
f32 weight;
};
LocalPose m_A; // 混合累积层(= 直接模式当前层)
std::vector<LocalPose> m_Layers; // N 层采样 scratch(容量跨帧复用)
std::vector<SampledLayer> m_Sampled; // 有效层配对 scratch
};
} // namespace Engine::Animation
#endif
@@ -0,0 +1,75 @@
//
// AnimationSystem:骨骼动画调度器(UE AnimInstance 求值管线 / Unity
// Animator Update 的分层对应物)。
//
// Update 每帧做三件事:
// 1. 逐实体推进 AnimationComponent 播放头 / crossfade 计时(状态层,
// 组件是唯一持久状态,System 只改写这些字段)
// 2. 调 AnimationEvaluator 把 clip 时间变成全节点 global 矩阵(求值层)
// 3. palette bump 分配写进 FrameSceneData(渲染消费契约)
//
// 与渲染的单向契约:System 写 skinPalettes / entitySkinBases /
// skinPaletteTruncated(+ 编辑形态 boneWireVerts),SceneRenderer 收集段
// 只读查表。动画推进与可见性无关——被剔除实体播放头恒走,回屏不跳变。
//
// 无状态机、无混合树(按需再演进):crossfade 就是全部过渡语义,
// 与既有 Play(index, fadeSeconds) 接口一一对应。
//
#ifndef HELLOVULKAN_ANIMATIONSYSTEM_H
#define HELLOVULKAN_ANIMATIONSYSTEM_H
#include <Engine/Core/Types.h>
#include <Engine/Animation/AnimationEvaluator.h>
#include <Engine/Renderer/SceneTypes.h>
#include <glm/glm.hpp>
#include <vector>
namespace Engine {
namespace ECS { class Scene; }
namespace Assets { class AssetManager; }
namespace Animation {
class AnimationSystem {
public:
// dt 单位秒。scene 只读遍历(组件状态字段除外——播放头
// 推进是 System 的本职写入);frame 是唯一产出出口
void Update(ECS::Scene& scene, Assets::AssetManager& assets,
f32 dt, FrameSceneData& frame);
private:
#ifdef HELLO_EDITOR
// 选中实体骨骼线框(Part B 可视化,仅编辑形态存在):
// 动画实体用当帧求值姿态,非动画/禁用实体用绑定姿势。
// 产出 frame.boneWireVerts,EditorGizmoPass 拼线绘制
void EmitSelectedSkeleton(ECS::Scene& scene,
Assets::AssetManager& assets,
FrameSceneData& frame);
#endif
AnimationEvaluator m_Evaluator;
std::vector<glm::mat4> m_Globals; // 求值输出 scratch
// Controller 模式混合层(BlendTree / 状态机过渡翻译产物)
std::vector<PoseLayer> m_LayerScratch;
std::vector<PoseLayer> m_FromLayers; // 过渡来源状态层
std::vector<std::pair<u32, f32>> m_BlendWeights; // 2D 树权重 scratch
#ifdef HELLO_EDITOR
// 选中实体当帧姿态暂存(求值循环内命中即拷,供
// EmitSelectedSkeleton 用;未命中/非动画 → 绑定姿势路径)
std::vector<glm::mat4> m_SelGlobals;
bool m_SelHasPose = false;
#endif
bool m_TruncWarned = false; // palette 预算溢出只告警一次
};
} // namespace Animation
} // namespace Engine
#endif
@@ -0,0 +1,168 @@
//
// HumanoidRig:Unity Avatar 语义的跨骨架重定向映射。
//
// 架构分层(对齐 Unity Mecanim / UE IK Retargeter 的做法):
//
// ┌─ standard ─ 标准人形拓扑:HumanoidSlot 52 槽(主干 6 + 臂链
// │ 4×2 + 指骨 15×2 + 腿链 3×2 + 趾 2),带侧别/镜像/
// │ 指骨结构化访问。重定向的"标准空间"。
// ├─ dialects ─ 命名方言数据表:各来源(Mixamo/Rigify/UE/MMD)的
// │ "槽位 → 归一化骨名"候选模式,纯数据无特判。命名
// │ 习惯差异全部收敛在这层(Unity 把 token 规则编成
// │ 数据表、UE 用链名映射,同思想:识别=查表)。
// └─ mapper ── 归一化(NormalizeLower → side 抽取 → 分隔符剥离)
// + 查表识别(MatchHumanoidSlot)+ 跨骨架配对
// (AutoMapHumanoid:同名直映优先,余下槽位一对一)。
//
// 动画重定向本身在 AnimationAttach:src 动画经映射对烘焙到 dst
// 骨架(两遍式 T-pose 参考搬运),本模块只负责"名字 → 槽位 → 配对"。
//
// 资产组装现实:角色模型与动画 clip 是两套独立资产,骨骼命名完全
// 不同源。同名直映是映射的特例(同源拆分资产零损耗)。
//
#ifndef HELLOVULKAN_HUMANOIDRIG_H
#define HELLOVULKAN_HUMANOIDRIG_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Model.h>
#include <glm/glm.hpp>
#include <string_view>
#include <utility>
#include <vector>
namespace Engine::Animation {
// ---- standard:标准人形槽位(Unity Avatar 52 骨拓扑)----
//
// 布局保证算术可推导(镜像/指骨反查都走位运算友好的分段):
// [0..5] 主干:Hips→Spine→Chest→UpperChest→Neck→Head
// [6..9] 左臂链:Shoulder→UpperArm→LowerArm→Hand
// [10..24] 左指骨:5 指 × 3 段(Thumb/Index/Middle/Ring/Pinky,
// 段 1=近节 → 3=末节,与 Mixamo HandThumb1-3、Rigify
// .01-.03、UE Proximal/Distal 对齐)
// [25..28] 左腿链:UpperLeg→LowerLeg→Foot→Toes
// [29..51] 右侧镜像(+23 偏移,镜像布局与左完全同构)
enum class HumanoidSlot : u32 {
Hips = 0,
Spine,
Chest,
UpperChest,
Neck,
Head,
LeftShoulder,
LeftUpperArm,
LeftLowerArm,
LeftHand,
LeftThumb1, // 指骨区起点(FingerSlot 算术依赖连续布局)
LeftThumb2,
LeftThumb3,
LeftIndex1,
LeftIndex2,
LeftIndex3,
LeftMiddle1,
LeftMiddle2,
LeftMiddle3,
LeftRing1,
LeftRing2,
LeftRing3,
LeftPinky1,
LeftPinky2,
LeftPinky3,
LeftUpperLeg,
LeftLowerLeg,
LeftFoot,
LeftToes,
RightShoulder,
RightUpperArm,
RightLowerArm,
RightHand,
RightThumb1,
RightThumb2,
RightThumb3,
RightIndex1,
RightIndex2,
RightIndex3,
RightMiddle1,
RightMiddle2,
RightMiddle3,
RightRing1,
RightRing2,
RightRing3,
RightPinky1,
RightPinky2,
RightPinky3,
RightUpperLeg,
RightLowerLeg,
RightFoot,
RightToes,
Count
};
// 槽位侧别(左右对称骨骼的识别结果)
enum class Side : u8 { None, Left, Right };
// 指别(0=拇指 … 4=小指;方言表里 Mixamo pinky / UE little 同槽)
enum class FingerKind : u32 { Thumb = 0, Index, Middle, Ring, Pinky };
// ---- standard:槽位结构化访问 ----
// 槽位侧别(主干槽返回 None;镜像侧与枚举名一致)
Side SideOfSlot(HumanoidSlot slot);
// 左右镜像(主干槽原样返回)
HumanoidSlot MirrorSlot(HumanoidSlot slot);
// 指骨槽位合成:(side, 指, 段 1..3) → 槽位
HumanoidSlot FingerSlot(Side side, FingerKind kind, u32 seg);
// 指骨槽位反解(非指骨槽位返回 false)
bool SlotFinger(HumanoidSlot slot, Side& side, FingerKind& kind,
u32& seg);
// 槽位显示名(Unity 语义:"Hips"/"LeftUpperArm"/"LeftHandThumb2"...;
// 调试映射表/Inspector 明细行用)
const char* HumanoidSlotName(HumanoidSlot slot);
// ---- mapper:名字归一化 + 槽位识别 ----
// 名字归一化 + 查方言表识别槽位。
// "mixamorig:LeftForeArm" → {LowerArm, Left};"左ひじ" → {LowerArm, Left};
// "Upper Arm_L" → {UpperArm, Left};"DEF-thumb.01.L" → {Thumb1, Left};
// "Thumb Proximal_R" → {Thumb1, Right}。识别失败 ok=false。
HumanoidSlot MatchHumanoidSlot(std::string_view name, Side& side,
bool& ok);
// 跨骨架自动映射:返回 (srcNodeIdx, dstNodeIdx) 对列表。
// 同名 node 直映优先(同源拆分资产零损耗),余下按槽位一对一配对;
// 同槽多候选时取方言表内的优先序(先高后低)。
std::vector<std::pair<u32, u32>> AutoMapHumanoid(
const std::vector<ModelNode>& srcNodes,
const std::vector<ModelNode>& dstNodes);
// 估计源/目标骨架的根对齐旋转(rest 姿态 basis 对齐,src 世界 →
// dst 世界)。跨骨架 retarget 时两套骨架常不同朝向(kwy DummyDoll
// Z-up vs X Bot Y-up),增量变换须经该旋转共轭才能跨空间传递。
// up = 头 - 双脚中点;right = 右臂 - 左臂(去 up 分量);
// fwd = up × right,脚趾朝向消歧。槽位不全(缺头/双脚/双臂)
// 或 rest 退化时返回 identity。
glm::quat EstimateHumanoidRootAlignment(
const std::vector<ModelNode>& srcNodes,
const std::vector<ModelNode>& dstNodes);
// 估计骨架的 T-pose 参考姿势(每节点**世界**旋转)。
// 人形动画资产的摆动量以 T-pose 为零位;rest 是 A-pose(臂下垂)
// 时,旋转增量直接搬运会过摆(臂越过中线)。此处把臂链
// (upper_arm 子树)绕肩刚体摆到水平侧向作为参考,其余骨取 rest
// (手指/趾走 rest 零位局部增量通道,见 AnimationAttach——指根
// 扇形编码在 rest 平移里,不参与世界系零位搬运);rest 本为
// T-pose 的资产摆动量≈0 自动退化。行走基不可用时全 rest。
std::vector<glm::quat> EstimateHumanoidTPose(
const std::vector<ModelNode>& nodes,
const std::vector<glm::mat4>& restGlobals);
} // namespace Engine::Animation
#endif // HELLOVULKAN_HUMANOIDRIG_H
@@ -1,37 +0,0 @@
//
// Created by atdunbg on 2026/9/22.
//
// AssetHandle:资产句柄 = (池内索引, 生成号),与 Entity/RG 句柄同一哲学。
//
// 组件、场景数据只存句柄;所有权归 AssetManager。
// 悬空句柄(generation 不匹配)会被 Get 显式拒绝,杜绝 use-after-free。
//
#ifndef HELLOVULKAN_ASSETHANDLE_H
#define HELLOVULKAN_ASSETHANDLE_H
#include <Engine/Core/Types.h>
// Model 前向声明(句柄不依赖完整类型)
namespace Engine { class Model; }
namespace Engine::Assets {
inline constexpr u32 kInvalidAssetIndex = 0xFFFFFFFF;
template<typename T>
struct AssetHandle {
u32 index = kInvalidAssetIndex;
u32 generation = 0;
bool Valid() const { return index != kInvalidAssetIndex; }
explicit operator bool() const { return Valid(); }
bool operator==(const AssetHandle& o) const = default;
};
// 常用别名
using ModelHandle = AssetHandle<Engine::Model>;
} // namespace Engine::Assets
#endif //HELLOVULKAN_ASSETHANDLE_H
+212 -46
View File
@@ -3,14 +3,9 @@
//
// AssetManager:引擎资产层。
//
// 职责划分(对照 Renderer):
// - 资产层:路径 → 解析/加载 → 缓存去重 → 句柄分发 + 生命周期管理
// - 渲染层:拿已解析资产创建/绑定 GPU 资源
//
// Model 的构造仍需要 GPU 上下文(buffer/image upload),这是现阶段的
// 折中:资产层"使用"GPU 层的接口,但 Renderer 不再知道磁盘和缓存。
// 后续演进:把 Model 拆成 CPU 解析数据 + GPU 上传两段,实现真正的
// 异步加载(IO 线程解析 → 主线程/上传队列提交),本文件结构已为此留好位置。
// 职责:路径 → 解析/加载 → 缓存去重 → 句柄分发 + 生命周期管理。
// GPU 创建不在这里:经 IAssetUploader 接口(渲染层提供实现)完成,
// 本类不感知 Vulkan——依赖方向 Assets → 接口,实现由组合根注入。
//
// 组件存句柄不存指针的理由:
// shared_ptr 会引入共享所有权 → 原子引用计数(swap-remove 拷贝时)
@@ -23,50 +18,56 @@
#include <Engine/Core/Types.h>
#include <Engine/Core/Log.h>
#include <Engine/Assets/AssetHandle.h>
#include <Engine/Core/AssetRef.h>
#include <Engine/Animation/AnimationAttach.h>
#include <Engine/Animation/AnimationController.h>
#include <Engine/Assets/IAssetUploader.h>
#include <Engine/Renderer/Model.h>
#include <Engine/Renderer/MaterialAsset.h>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
namespace Engine {
class Device;
class MemoryAllocator;
class DescriptorManager;
namespace Assets {
namespace Engine
{
namespace Assets
{
// ---------- 通用资产缓存(不感知 Vulkan / 磁盘细节由 loader 决定) ----------
template <typename T>
class AssetCache {
class AssetCache
{
public:
// loader:path → 资产实例(失败返回 nullptr)
using LoaderFn = std::function<Scope<T>(const std::string&)>;
// 加载(带路径去重)。失败返回无效句柄。
AssetHandle<T> Load(const std::string& path, const LoaderFn& loader)
AssetRef<T> Load(const std::string& path, const LoaderFn& loader)
{
if (auto it = m_ByPath.find(path); it != m_ByPath.end())
{
if (auto it = m_ByPath.find(path); it != m_ByPath.end()) {
return it->second;
}
Scope<T> asset = loader(path);
if (!asset) {
if (!asset)
{
ENGINE_ERROR("AssetCache: failed to load '{}'", path);
return {};
}
AssetHandle<T> h;
if (!m_Free.empty()) {
AssetRef<T> h;
if (!m_Free.empty())
{
h.index = m_Free.back();
m_Free.pop_back();
h.generation = m_Entries[h.index].generation;
m_Entries[h.index] = Entry{path, h.generation, std::move(asset), true};
} else {
}
else
{
h.index = static_cast<u32>(m_Entries.size());
h.generation = 1;
m_Entries.push_back(Entry{path, h.generation, std::move(asset), true});
@@ -77,14 +78,16 @@ namespace Engine {
}
// 句柄 → 资产指针。悬空句柄 FATAL(bug 信号);已卸载返回 nullptr。
T* Get(AssetHandle<T> h) const
T* Get(AssetRef<T> h) const
{
if (!h.Valid() || h.index >= m_Entries.size())
{
if (!h.Valid() || h.index >= m_Entries.size()) {
ENGINE_FATAL("AssetCache::Get: invalid handle (index={}, gen={})",
h.index, h.generation);
}
const Entry& e = m_Entries[h.index];
if (e.generation != h.generation) {
if (e.generation != h.generation)
{
ENGINE_FATAL("AssetCache::Get: stale handle for '{}' (gen {} != {})",
e.path, h.generation, e.generation);
}
@@ -94,7 +97,7 @@ namespace Engine {
// 卸载:句柄即刻失效(generation 递增),路径缓存清除。
// 注意:GPU 侧何时安全释放由上层(Renderer fence)保证——
// 本类只管所有权,不管 GPU 生命周期。
void Unload(AssetHandle<T> h)
void Unload(AssetRef<T> h)
{
if (!h.Valid() || h.index >= m_Entries.size()) return;
@@ -109,7 +112,15 @@ namespace Engine {
m_Free.push_back(h.index);
}
const std::string& Path(AssetHandle<T> h) const
// 按路径卸载(未加载为 no-op;编辑器资产重建用——
// 手上只有路径没有句柄的场景)
void UnloadByPath(const std::string& path)
{
auto it = m_ByPath.find(path);
if (it != m_ByPath.end()) Unload(it->second);
}
const std::string& Path(AssetRef<T> h) const
{
static const std::string kEmpty;
if (!h.Valid() || h.index >= m_Entries.size()) return kEmpty;
@@ -117,13 +128,31 @@ namespace Engine {
return (e.generation == h.generation && e.alive) ? e.path : kEmpty;
}
// 按路径查活资产(未加载/已卸载返回 nullptr;不触发加载、
// 不对坏句柄 FATAL)——面板展示用
T* Find(const std::string& path) const
{
auto it = m_ByPath.find(path);
if (it == m_ByPath.end()) return nullptr;
const Entry& e = m_Entries[it->second.index];
return (e.alive && e.generation == it->second.generation)
? e.asset.get() : nullptr;
}
u32 Count() const
{
return static_cast<u32>(m_Entries.size()) - static_cast<u32>(m_Free.size());
}
// 路径是否在缓存(含被卸载后路径清除的语义:卸载即不含)
bool Contains(const std::string& path) const
{
return m_ByPath.find(path) != m_ByPath.end();
}
private:
struct Entry {
struct Entry
{
std::string path;
u32 generation = 1;
Scope<T> asset;
@@ -132,44 +161,181 @@ namespace Engine {
std::vector<Entry> m_Entries;
std::vector<u32> m_Free;
std::unordered_map<std::string, AssetHandle<T>> m_ByPath;
std::unordered_map<std::string, AssetRef<T>> m_ByPath;
};
// ---------- AssetManager:全引擎资产入口 ----------
// (ModelHandle 别名来自 AssetHandle.h)
class AssetManager {
class AssetManager
{
public:
// GPU 上下文由组合根(Application)在 Renderer 装配完成后注入
AssetManager(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
vk::DescriptorSetLayout textureLayout);
// 上传服务由组合根(Application)注入:渲染层实现接口,
// 资产层不感知 Vulkan 具体类
explicit AssetManager(IAssetUploader& uploader);
~AssetManager() = default;
AssetManager(const AssetManager&) = delete;
AssetManager& operator=(const AssetManager&) = delete;
ModelHandle LoadModel(const std::string& path);
Model* GetModel(ModelHandle h) const { return m_Models.Get(h); }
void UnloadModel(ModelHandle h) { m_Models.Unload(h); }
ModelRef LoadModel(const std::string& path);
// 异步路径:解析已在后台线程完成(路径已缓存则产物丢弃去重)
ModelRef LoadModel(const std::string& path, ParsedModel&& parsed);
Model* GetModel(ModelRef h) const { return m_Models.Get(h); }
void UnloadModel(ModelRef h) { m_Models.Unload(h); }
// 批量按路径卸载模型(未加载为 no-op)。先 GPU 全停飞再销毁
//(在飞命令缓冲可能仍引用顶点缓冲/默认纹理 sampler——
// 与 ReloadMaterialAsset 的 WaitDeviceIdle 同一先例);一次
// 停飞摊薄整批,调用方攒批降频(脚本碎片回收每帧一批)
void UnloadModelsByPaths(const std::vector<std::string>& paths)
{
if (paths.empty()) return;
m_Uploader.WaitDeviceIdle();
for (const std::string& p : paths) {
m_Models.UnloadByPath(p);
}
}
// 按路径查已加载的模型(未加载返回 null,不触发加载)——
// 面板 hover 信息卡用
Model* GetModelByPath(const std::string& path) const
{
return m_Models.Find(path);
}
// ---- 外部动画并入(UE AnimSequence 归属 Skeleton 的简化版)----
// 把动画容器文件(无网格 FBX/GLB 等)里的 clip 按骨骼节点名
// 重映射进目标模型的 clip 表。前提 = 源/目标骨骼命名同源(如
// 同一 DCC 拆分/Mixamo 同角色);名字对不上的轨道跳过并计数。
// 幂等:目标 clips 已含该源(sourcePath)时不重复并入。
// 本方法只是 facade(取模型 + 解析磁盘),重映射算法在
// Animation::AttachClipsToModel
using AnimAttachResult = Animation::AttachResult;
AnimAttachResult AttachAnimationClips(ModelRef target,
const std::string& animPath);
// 对称剥离(编辑器 detach):删除目标模型里来自 animPath 的
// clip,返回删除数。facade(取模型 + 调 Model 方法)
u32 DetachAnimationClips(ModelRef target,
const std::string& animPath)
{
Model* m = GetModel(target);
return m ? m->RemoveClipsBySource(animPath) : 0u;
}
// 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。
ModelHandle CreatePlane(f32 halfSize, u8 r, u8 g, u8 b);
ModelRef CreatePlane(f32 halfSize, u8 r, u8 g, u8 b);
// 半透明平面(alpha 0-255,透明绘制队列):同参数缓存去重
ModelRef CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a);
// 程序化立方体(halfSize 为半边长):同参数缓存去重。
// 尺寸变化走实体 Transform 缩放,勿按尺寸炸缓存
ModelRef CreateCube(f32 halfSize, u8 r, u8 g, u8 b);
// 通用程序化图元(MeshFactory 形状名 + 参数):同形状同参数同色
// 缓存去重。几何唯一源 = MeshFactory(见 MeshFactory.h)
ModelRef CreatePrimitive(const std::string& shape,
const PrimitiveDesc& desc,
u8 r, u8 g, u8 b);
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
// 重复 Load 同一路径零成本(动态切换场景直接反复调用)
EnvRef LoadEnvironment(const std::string& path);
// 异步路径:解码已在后台线程完成(若路径已缓存,解码产物被
// 丢弃去重——上层可先查缓存再起 worker,此处不做强约束)
EnvRef LoadEnvironment(const std::string& path,
DecodedEquirect&& decoded);
Cubemap* GetEnvironment(EnvRef h) const { return m_Environments.Get(h); }
// 路径缓存查询(面板显示加载状态用;不触发加载)
bool IsModelLoaded(const std::string& path) const
{
return m_Models.Contains(path);
}
bool IsEnvironmentLoaded(const std::string& path) const
{
return m_Environments.Contains(path);
}
// ---- 材质资产(.material,YAML → GPU)----
// 同步加载(YAML 解析 + 纹理上传,主线程):路径缓存去重,
// 失败返回无效句柄。调用点收敛在两处——场景读后预载
//(SceneSerializer,游戏形态也走)与编辑器指派/创建覆盖
//(Inspector);渲染收集期只查不载(FindMaterialAsset)
MaterialAssetRef LoadMaterialAsset(const std::string& path);
// 按路径查已加载材质(未加载返回 null,不触发加载)——
// 收集期 resolve / 面板展示用
MaterialAsset* FindMaterialAsset(const std::string& path) const
{
return m_Materials.Find(path);
}
bool IsMaterialLoaded(const std::string& path) const
{
return m_Materials.Contains(path);
}
// 编辑器重建路径(纹理槽变化):GPU 全停飞 → 卸载旧资产
//(句柄即刻失效,引用方下帧 resolve 自然拿到新实例)→
// 重新加载。waitIdle 经 uploader 接缝(本类不感知 Vulkan)
MaterialAssetRef ReloadMaterialAsset(const std::string& path);
// ---- 动画控制器资产(.animctrl,YAML → 状态机数据)----
// 模式同 MaterialAsset:加载点收敛两处(场景读后预载 +
// 编辑器指派),System 收集期只 Find 不 Load。组件按路径
// 引用(controllerPath),句柄仅指派点短暂持有
using AnimCtrlRef = AssetRef<Animation::AnimationController>;
AnimCtrlRef LoadController(const std::string& path);
// 强制重载(.animctrl = 纯文本 YAML,外部编辑器改完后调用;
// 失败时缓存已清,Find 落空 → rest 兜底,路径仍可再次重载)
AnimCtrlRef ReloadController(const std::string& path)
{
m_Controllers.UnloadByPath(path);
return LoadController(path);
}
Animation::AnimationController* FindController(const std::string& path) const
{
return m_Controllers.Find(path);
}
bool IsControllerLoaded(const std::string& path) const
{
return m_Controllers.Contains(path);
}
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
// 各自一个 AssetCache<T> + 便捷方法,模式与 Model 完全一致。
// 把 dirty 的 GUID 注册表落盘(Application 退出 / 场景保存时调)。
// 内部幂等:非 dirty 直接 return。GetOrCreateGUID 只标 dirty 不落盘,
// 避免每次新增 GUID 都全量重写 yaml(开发期编辑器卡顿主因之一)
void FlushGUIDRegistry();
private:
Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_Descriptors;
vk::DescriptorSetLayout m_TextureLayout;
void LoadGUIDRegistry();
void SaveGUIDRegistry() const;
IAssetUploader& m_Uploader;
AssetCache<Model> m_Models;
};
AssetCache<Cubemap> m_Environments;
AssetCache<MaterialAsset> m_Materials;
AssetCache<Animation::AnimationController> m_Controllers;
// GUID 注册表(双向);GetOrCreateGUID 标 dirty,FlushGUIDRegistry 落盘
// (Application 退出 / 场景保存时统一 flush,避免每次新增都全量重写 yaml)
std::unordered_map<u64, std::string> m_GuidToPath;
std::unordered_map<std::string, u64> m_PathToGuid;
bool m_GuidRegistryDirty = false;
};
} // namespace Assets
} // namespace Engine
+55
View File
@@ -0,0 +1,55 @@
//
// Created by atdunbg on 2026/9/23.
//
// DDS 文件解析(最小实现,不解码压缩块)。
//
// 设计:GPU 原生支持 BC 压缩格式——DDS 的压缩数据**直接上传**为
// BC1/BC2/BC3/BC4/BC5/BC6H/BC7 纹理由硬件采样解码,不做 CPU 解码。
// 输出中性 DdsFormat(本层禁 Vulkan 头),vk::Format 映射与 sRGB
// 语义由调用方(Texture,渲染侧)决定。
//
// 支持:
// - legacy fourcc:DXT1/DXT3/DXT5/ATI1/BC4U/ATI2/BC5U
// - DX10 扩展头:BC1..BC7、R8G8B8A8、B8G8R8A8
// - 未压缩 32bpp(DDPF_RGB | DDPF_ALPHAPIXELS)
// - 2D + mip 链(cubemap 面标志暂不支持,报错)
//
// 输出:Mips[r] = 第 r 级完整数据(BC 按块行紧凑排布:
// bytes = ceil(w/4)*ceil(h/4)*blockBytes)。
//
#ifndef HELLOVULKAN_DDSLOADER_H
#define HELLOVULKAN_DDSLOADER_H
#include <Engine/Core/Types.h>
#include <string>
#include <vector>
namespace Engine::Assets::Dds {
// 中性格式(Assets 层不感知 Vulkan;映射在渲染侧 Texture)
enum class DdsFormat {
BC1, BC2, BC3, BC4, BC5, BC6H, BC7,
RGBA8,
BGRA8, // 按字节序上传后视作 RGBA(近似,触发警告)
};
struct DdsData {
u32 Width = 0;
u32 Height = 0;
u32 MipLevels = 1;
DdsFormat Format = DdsFormat::RGBA8;
std::vector<std::vector<u8>> Mips; // 每级完整数据(紧凑排布)
bool BlockCompressed = false; // BC 格式(rowPitch 按块计算)
};
// 解析 DDS 文件;失败返回 false 并写 errMsg
bool Load(const std::string& path, DdsData& out, std::string& errMsg);
// 指定 mip 级的单行字节跨度(BC = 块行;RGBA8 = w*4)
u32 RowPitch(const DdsData& data, u32 mipLevel);
} // namespace Engine::Assets::Dds
#endif //HELLOVULKAN_DDSLOADER_H
@@ -0,0 +1,75 @@
//
// Created by atdunbg on 2026/9/23.
//
// IAssetUploader:资产层 → GPU 的上传接口。
//
// 资产层(AssetManager/AssetCache)只认这个接口,不感知 Vulkan 具体类
// (Device/Allocator/DescriptorManager)。实现由渲染层提供
// (VulkanAssetUploader),组合根(Renderer)装配后注入。
//
// 对齐 UE:资产系统(AssetManager/StreamableManager)与渲染资源创建
// (rendering thread / RHI)分离,资产层只拿到"可用的渲染资源"。
//
// 后续演进点:接口加返回 future / 回调即为异步加载
// (IO 线程解析 → 上传队列提交),本接口已是正确的接缝。
//
#ifndef HELLOVULKAN_IASSETUPLOADER_H
#define HELLOVULKAN_IASSETUPLOADER_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Model.h>
#include <Engine/Renderer/MaterialAsset.h>
#include <Engine/Renderer/Cubemap.h>
namespace Engine::Assets {
class IAssetUploader {
public:
virtual ~IAssetUploader() = default;
// 磁盘模型 → 解析 + GPU 上传(失败返回 nullptr)
virtual Scope<Model> LoadModel(const std::string& path) = 0;
// 异步路径:解析已在后台线程完成,此调用只做 GPU 构建(主线程)
virtual Scope<Model> LoadModel(const std::string& path,
ParsedModel&& parsed) = 0;
// 程序化纯色平面(size 为半边长)
virtual Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) = 0;
// 半透明纯色平面(alpha 0-255,透明绘制队列)
virtual Scope<Model> CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a) = 0;
// 程序化纯色立方体(halfSize 为半边长;占位几何/碎片模拟)
virtual Scope<Model> CreateCube(f32 halfSize, u8 r, u8 g, u8 b) = 0;
// 通用程序化图元(MeshFactory 形状名:cube/plane/sphere/cylinder/
// cone/torus 等):形状 + 参数 → 单 mesh 模型。非法形状返回 nullptr
virtual Scope<Model> CreatePrimitive(const std::string& shape,
const PrimitiveDesc& desc,
u8 r, u8 g, u8 b) = 0;
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
// 异步加载路径:解码已在后台线程完成,此调用只做 GPU 生成(主线程)
virtual Scope<Cubemap> LoadEnvironment(const std::string& path,
DecodedEquirect&& decoded) = 0;
// ---- 材质资产 GPU 构建(MaterialAsset 专用接缝)----
// 数据段 → 纹理上传 + Material 描述符(失败/空槽折共享默认纹理,
// 纹理所有权归返回的 MaterialAsset)。失败返回 nullptr
virtual Scope<MaterialAsset> CreateMaterial(
MaterialAssetData&& data) = 0;
// GPU 全停飞(编辑器低频重建路径用:材质资产 Reload 前调用,
// 保证旧纹理/描述符无在飞引用后销毁——与 Renderer::SetEnvironment
// 的 waitIdle 换绑同一先例)
virtual void WaitDeviceIdle() = 0;
};
} // namespace Engine::Assets
#endif //HELLOVULKAN_IASSETUPLOADER_H
+39 -8
View File
@@ -9,13 +9,14 @@
// 和事件钩子(OnEvent + 类型化事件)定义程序行为;
// runtime、editor 等不同程序形态只是不同的派生类。
//
// 主循环固定为:PollEvents → OnUpdate(dt) → [NewFrame → OnRenderOverlay →
// 主循环固定为:PollEvents → OnUpdate(FrameTime) → [NewFrame → OnRenderOverlay →
// BuildDrawData] → 主渲染(Renderer.Render)→ 判退出。
// UI 构建与 GPU 提交分离:OnRenderOverlay 只"声明"控件,真正的 draw data
// 提交发生在主渲染管线中的 ImGui pass(Forward 之后、Present 之前,
// loadOp=Load 叠加)。主渲染是框架行为,派生类不可覆盖也不会丢失;
// 框架自身永远不绘制任何 ImGui 控件——UI 内容全部由派生类定义。
// dt 由框架统一计时(含首帧与卡顿钳制),派生层无需自己管理。
// 时间由框架单源采集:帧首 Time::BeginFrame 落定 FrameTime(含卡顿钳制),
// 派生层经 OnUpdate(frame) 或 Time 门面只读,不再自己碰 chrono。
//
#ifndef HELLOVULKAN_APPLICATION_H
@@ -23,21 +24,27 @@
#include <Engine/Core/Types.h>
#include <Engine/Core/Event.h>
#include <Engine/Core/GameFlow.h>
#include <Engine/Core/Time.h>
#include <chrono>
#include <string>
#include "Engine/ImGui/ImGuiLayer.h"
namespace Engine {
class Window;
class Renderer;
class ImGuiLayer;
class PhysicsWorld;
namespace Scripting { class ScriptRuntime; }
namespace ECS { class Scene; }
namespace Assets { class AssetManager; }
class Application {
public:
Application(std::string title, u32 width, u32 height);
Application(const std::string& title, u32 width, u32 height);
virtual ~Application();
Application(const Application&) = delete;
@@ -54,26 +61,42 @@ namespace Engine {
// 主渲染(ImGui pass 把 draw data 叠加到场景上)。
void EnableImGui();
// ---- 帧率控制 ----
// vsync 开启时 present 自带节奏,手动限帧不生效;
// 限帧目标最低 24fps(防止过低刷新导致交互卡死),无上限
void SetVSync(bool vsync);
bool GetVSync() const { return m_VSync; }
void SetTargetFps(f32 fps) { m_TargetFps = std::max(24.0f, fps); }
f32 GetTargetFps() const { return m_TargetFps; }
// ---- 供派生类访问的模块 ----
Window& GetWindow() const { return *m_Window; }
Renderer& GetRenderer() const { return *m_Renderer; }
ECS::Scene& GetScene() const { return *m_Scene; }
Assets::AssetManager& GetAssets() const { return *m_Assets; }
f32 GetDeltaTime() const { return m_DeltaTime; }
PhysicsWorld& GetPhysics() const { return *m_Physics; }
Scripting::ScriptRuntime& GetScripts() const { return *m_Scripts; }
GameFlow& GetGameFlow() { return m_GameFlow; }
u64 GetFrameCount() const { return m_FrameCount; }
// ImGui相关
void SetImGuiEnabled(const bool enable) { if (m_ImGuiLayer) m_ImGuiLayer->SetEnabled(enable); }
// 理论上ImGuiLayer不应该会空,暂时静默处理
bool GetImGuiEnabled() const { return m_ImGuiLayer ? m_ImGuiLayer->IsEnabled() : false; }
protected:
// ---- 生命周期钩子 ----
// 框架装配完成(Window/Renderer 可用),派生类做初始化(建场景等)
virtual void OnInit() {}
// 每帧逻辑更新(输入、动画、物理……)
virtual void OnUpdate(f32 deltaTime) {}
// 每帧逻辑更新(输入、动画、物理……)。frame 当帧只读:
// Dt() = scaled(游戏逻辑),UnscaledDt() = 真实(编辑器相机/UI)
virtual void OnUpdate(const FrameTime& frame) {}
// 主渲染之后、当帧提交之前的叠加层钩子(ImGui/调试视图/编辑器 UI)。
// 主渲染由框架固定执行;这里只负责"额外的"渲染,不覆写则无叠加层。
virtual void OnRenderOverlay() {}
virtual void OnImGuiRender() {}
// 主循环结束后、析构前(保存、清理场景等)
virtual void OnShutdown() {}
@@ -100,14 +123,22 @@ namespace Engine {
Scope<ECS::Scene> m_Scene;
Scope<Renderer> m_Renderer;
Scope<Assets::AssetManager> m_Assets;
Scope<PhysicsWorld> m_Physics; // Jolt 物理(Run 起循环时创建)
Scope<Scripting::ScriptRuntime> m_Scripts; // 脚本生命周期(Run 起循环时创建)
// 注意成员序:ImGuiLayer 必须后声明(先于 Renderer/Window 析构,
// 其后端销毁需要 Device 与 GLFWwindow 存活)
Scope<ImGuiLayer> m_ImGuiLayer;
// 游戏流状态机(构造即冻结:Stopped → timeScale=0)
GameFlow m_GameFlow;
bool m_Running = false;
f32 m_DeltaTime = 0.0f;
u64 m_FrameCount = 0;
// 帧率控制
bool m_VSync = true; // 垂直同步(present 自带节奏)
f32 m_TargetFps = 60.0f; // 限帧目标(≥24)
std::chrono::steady_clock::time_point m_LastFrameTime;
};
+49
View File
@@ -0,0 +1,49 @@
//
// Created by atdunbg on 2026/9/23.
//
// AssetRef:中性资产引用 = (池内索引, 生成号),与 Entity/RG 句柄同一哲学。
//
// 放在 Core 层(不依赖 Assets/Vulkan):ECS 组件、序列化数据只存这个
// 纯值类型。对齐 UE/Unity 的做法——组件持有"资产引用",但不感知资产
// 系统内部:Unity 的 InstanceID、UE 的 FSoftObjectPath 本质都是中性引用
// 值,解析归资产系统(AssetManager)。
// 悬空引用(generation 不匹配)由 AssetCache::Get 显式拒绝。
//
#ifndef HELLOVULKAN_ASSETREF_H
#define HELLOVULKAN_ASSETREF_H
#include <Engine/Core/Types.h>
// 资产类型前向声明(必须放全局层:namespace 内前向声明会创建
// Engine::Assets::Engine::Model 这类错误嵌套命名空间)
namespace Engine {
class Model;
class Cubemap;
class Texture;
class Material;
class MaterialAsset;
}
namespace Engine {
inline constexpr u32 kInvalidAssetIndex = 0xFFFFFFFF;
template<typename T>
struct AssetRef {
u32 index = kInvalidAssetIndex;
u32 generation = 0;
bool Valid() const { return index != kInvalidAssetIndex; }
explicit operator bool() const { return Valid(); }
bool operator==(const AssetRef& o) const = default;
};
// 常用别名(新增资产类型时在这里加一行)
using ModelRef = AssetRef<Model>;
using EnvRef = AssetRef<Cubemap>; // 环境(IBL 四件套)
using MaterialAssetRef = AssetRef<MaterialAsset>; // 材质资产(.material)
} // namespace Engine
#endif //HELLOVULKAN_ASSETREF_H
+82
View File
@@ -0,0 +1,82 @@
//
// Created by atdunbg on 2026/9/24.
//
// AsyncTask:单 in-flight 异步任务的通用收割工具(header-only)。
//
// 异步加载的公共骨架(已验证两次:环境 DecodeHdr / 模型 ParseFromDisk):
// worker 线程跑纯 CPU 任务 → 原子标志通知 → 主线程每帧轮询收割。
// Vulkan 提交/描述符等 GPU 工作全部留在主线程(设备非线程安全)。
//
// AsyncTask<ParsedModel> task;
// task.Run([] { return Model::ParseFromDisk(path); }); // 发起
// if (task.Done()) { auto p = task.Take(); ... } // 每帧收割
// // 析构自动 join(worker 只做纯 CPU 工作,join 恒安全)
//
// 语义:Run 在忙碌时先 join 并丢弃旧结果(覆盖式);"快速连切只保留
// 最终意图"的 pending 接力由调用方持有(参照 main.cpp 环境加载)。
//
#ifndef HELLOVULKAN_ASYNCTASK_H
#define HELLOVULKAN_ASYNCTASK_H
#include <Engine/Core/Types.h>
#include <atomic>
#include <functional>
#include <thread>
#include <utility>
namespace Engine {
template <typename T>
class AsyncTask {
public:
AsyncTask() = default;
~AsyncTask() { Join(); }
AsyncTask(const AsyncTask&) = delete;
AsyncTask& operator=(const AsyncTask&) = delete;
// 发起任务。忙碌时先等待旧任务完成并放弃其结果。
void Run(std::function<T()> job)
{
Join();
m_Busy = true;
m_Done.store(false);
m_Worker = std::thread([this, j = std::move(job)]() {
m_Result = j();
m_Done.store(true); // release:结果对主线程 acquire 可见
});
}
bool Busy() const { return m_Busy; }
bool Done() const { return m_Busy && m_Done.load(); }
// 收割:join worker 并移交结果。只在 Done() 为 true 时调用。
T Take()
{
m_Worker.join(); // join = 同步点,此后结果归主线程
m_Busy = false;
T out = std::move(m_Result);
m_Result = T{};
return out;
}
// 关闭前兜底等待(析构自动调用)
void Join()
{
if (m_Worker.joinable()) {
m_Worker.join();
}
m_Busy = false;
}
private:
std::thread m_Worker;
std::atomic<bool> m_Done{false};
T m_Result{};
bool m_Busy = false;
};
} // namespace Engine
#endif //HELLOVULKAN_ASYNCTASK_H
+71
View File
@@ -0,0 +1,71 @@
//
// GameFlow.h:游戏流状态机(引擎层唯一的"游戏在干什么"权威)。
//
// 状态:Stopped(编辑态/未启动)→ Loading(加载中)→ Running ⇄ Paused,
// 任意活动态可 RequestStop 回 Stopped。转换矩阵外的请求拒绝 + warn。
//
// 职责边界:
// - timeScale 单一权威:每次转换统一设 Time::SetTimeScale
// (Running/Loading→1,Paused/Stopped→0)。上层(编辑器/游戏)
// 不再自行碰 timeScale——冻结语义由状态推导,不再散落
// - 状态转换广播:AddListener 注册监听(组合根/编辑器 Init 期注册,
// 非每帧路径)。编辑器在 →Loading 挂场景快照、→Stopped 挂恢复;
// 脚本/系统将来同样可订阅(跨系统解耦)
// - 不知场景不知资产:加载本身(读文件/反序列化)是调用方的事,
// 本类只管状态与转换纪律(CompleteLoading 由加载方显式调)
//
// 与主循环的契约(Application::Run):
// - Input::Poll 仅 Running 激活(Paused/Stopped 动作静默)
// - 物理步进仅 Loading 跳过(编辑态对账照跑,停摆靠 dt=0)
// - 脚本仅 Running 执行(Unity Play Mode / UE PIE 语义——脚本副
// 作用不依赖 dt,编辑态不得运行);→Stopped 由监听 EndPlay 终
// 局脚本实例(组合根注册,先于编辑器回滚监听)
//
#ifndef HELLOVULKAN_GAMEFLOW_H
#define HELLOVULKAN_GAMEFLOW_H
#include "Engine/Core/Types.h"
#include <functional>
#include <vector>
namespace Engine {
enum class GameFlowState : u8 {
Stopped, // 编辑态 / 未启动(timeScale=0)
Loading, // 加载中(物理/脚本步进跳过;timeScale=1 无消费方)
Running, // 游戏运行(timeScale=1)
Paused, // 暂停(timeScale=0;编辑器 Pause / 游戏菜单挂起)
};
class GameFlow {
public:
// (from, to);注册期 = 组合根/编辑器 Init,非每帧路径
using Listener = std::function<void(GameFlowState, GameFlowState)>;
GameFlow(); // 构造即冻结(Stopped 语义:timeScale=0)
void AddListener(Listener l) { m_Listeners.push_back(std::move(l)); }
GameFlowState State() const { return m_State; }
bool IsIn(GameFlowState s) const { return m_State == s; }
// ---- 转换请求(矩阵外拒绝 + warn,返回是否生效)----
bool RequestStart(); // Stopped → Loading
void CompleteLoading(); // Loading → Running(加载方显式调)
bool RequestPause(); // Running → Paused
bool RequestResume(); // Paused → Running
bool RequestStop(); // Loading/Running/Paused → Stopped
private:
// 立即生效转换:设 timeScale + 广播(同既有 DoPlay 同步语义)
void Transition(GameFlowState to);
GameFlowState m_State = GameFlowState::Stopped;
std::vector<Listener> m_Listeners;
};
} // namespace Engine
#endif //HELLOVULKAN_GAMEFLOW_H
+140
View File
@@ -0,0 +1,140 @@
//
// Input.h:统一输入门面(脚本/游戏逻辑唯一入口)。
//
// 目的:
// 1. 上层代码(脚本、SandboxApp、编辑器相机)不再直接依赖 GLFW——
// 只依赖本枚举与门面,Window 后端将来可换(SDL/平台原生)
// 2. 嵌入脚本时暴露的也是这一套(Lua/Python 绑定包一层即可)
//
// 键码值 = GLFW 物理键码(glf3.h 标准值),强转 int 可直接传给
// Window::IsKeyPressed;但调用方不得依赖这一点。
//
// 语义:轮询接口沿用 Window 的门控规则——UI 捕获键盘(编辑器文本框
// 输入中)时 IsKeyDown 返回 false,游戏侧静默;鼠标捕获同理。
//
#pragma once
#include <glm/glm.hpp>
#include "Engine/Core/Types.h"
#include "Engine/Input/InputAction.h"
namespace Engine {
class Window;
namespace Input {
enum class Key : int {
// ---- 可打印区(值 = ASCII)----
Space = 32,
Apostrophe = 39,
Comma = 44,
Minus = 45,
Period = 46,
Slash = 47,
Num0 = 48, Num1 = 49, Num2 = 50, Num3 = 51, Num4 = 52,
Num5 = 53, Num6 = 54, Num7 = 55, Num8 = 56, Num9 = 57,
Semicolon = 59,
Equal = 61,
A = 65, B = 66, C = 67, D = 68, E = 69, F = 70, G = 71,
H = 72, I = 73, J = 74, K = 75, L = 76, M = 77, N = 78,
O = 79, P = 80, Q = 81, R = 82, S = 83, T = 84, U = 85,
V = 86, W = 87, X = 88, Y = 89, Z = 90,
BracketLeft = 91,
Backslash = 92,
BracketRight = 93,
GraveAccent = 96,
// ---- 控制键(值 = GLFW 键码)----
Escape = 256,
Enter = 257,
Tab = 258,
Backspace = 259,
Insert = 260,
Delete = 261,
Right = 262,
Left = 263,
Down = 264,
Up = 265,
PageUp = 266,
PageDown = 267,
Home = 268,
End = 269,
F1 = 290, F2 = 291, F3 = 292, F4 = 293, F5 = 294, F6 = 295,
F7 = 296, F8 = 297, F9 = 298, F10 = 299, F11 = 300,
F12 = 301,
// ---- 修饰键 ----
LeftShift = 340,
LeftControl = 341,
LeftAlt = 342,
LeftSuper = 343,
RightShift = 344,
RightControl = 345,
RightAlt = 346,
RightSuper = 347,
};
enum class MouseButton : int {
Left = 0, // GLFW_MOUSE_BUTTON_1
Right = 1,
Middle = 2,
};
// 装配:Application 构造时自动调用(后端 Window 就绪后)。
// 非 const:门面含状态设置(光标捕获),不只是查询
void Init(Window& window);
// 轮询查询(UI 捕获对应输入时返回 false——游戏侧静默)
bool IsKeyDown(Key key);
bool IsMouseButtonDown(MouseButton button);
// ---- 光标捕获(自由视角三件套:隐藏光标 + 无界鼠标增量 +
// 滚轮增量)。语义沿 Window 后端:捕获时光标隐藏、位置锁定
// 窗口中心,GetMouseDelta 出无界增量;重复设置同值无操作 ----
// 隐藏并锁定光标(自由视角/环绕中);false 恢复正常光标
void SetCursorCaptured(bool captured);
bool IsCursorCaptured();
// 帧间光标增量(无界,捕获态下才有意义;窗口尺寸/模式变化
// 由后端参考点重置防跳变)
void GetMouseDelta(f32& dx, f32& dy);
// 滚轮增量(上行 = +1,一档 ≈1):读后清零。UI 捕获鼠标
//(面板悬停)时不累积——滚轮归 ImGui
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
+6
View File
@@ -11,6 +11,12 @@
namespace Engine {
namespace Logger
{
void Initialize();
void Shutdown();
}
#define ENGINE_TRACE(...) ::spdlog::trace(__VA_ARGS__)
#define ENGINE_INFO(...) ::spdlog::info(__VA_ARGS__)
#define ENGINE_DEBUG(...) ::spdlog::debug(__VA_ARGS__)
+113
View File
@@ -0,0 +1,113 @@
//
// LogRing.h:进程级日志环形缓冲(编辑器 Log Panel 的数据源)。
//
// RingSink 注册进默认 logger 的 sinks 后,所有日志(引擎 + 脚本经
// ScriptLog/ENGINE_* 打印的)自动落缓冲;面板按 version 增量感知、
// Snapshot 整体拉取。头文件设施(sink_it_ 内联),无 cpp。
//
#ifndef HELLOVULKAN_LOGRING_H
#define HELLOVULKAN_LOGRING_H
#include <Engine/Core/Types.h>
#include <spdlog/sinks/base_sink.h>
#include <spdlog/details/log_msg.h>
#include <atomic>
#include <mutex>
#include <string>
#include <vector>
namespace Engine::Debug {
struct LogEntry {
spdlog::level::level_enum level;
std::string msg; // 整行(已按 ring pattern 格式化,无色码)
};
class LogRingBuffer {
public:
static constexpr size_t kCap = 2048;
// 门控:启动期系统初始化日志(Vulkan/UBO/IBL...)不进面板,
// 与 Unity Console 语义一致(只收运行期游戏侧日志)。
// Application::Run 进入主循环时开闸
bool IsEnabled() const {
return m_Enabled.load(std::memory_order_relaxed);
}
void SetEnabled(bool v) {
m_Enabled.store(v, std::memory_order_relaxed);
}
void Push(spdlog::level::level_enum lvl, std::string msg) {
std::lock_guard<std::mutex> lk(m_Mutex);
if (m_Entries.size() < kCap) {
m_Entries.push_back({lvl, std::move(msg)});
} else {
m_Entries[m_Head] = {lvl, std::move(msg)};
m_Head = (m_Head + 1) % kCap; // 满后覆写最旧
}
++m_Version;
}
// 快照(满环时按时间序重排)
std::vector<LogEntry> Snapshot() const {
std::lock_guard<std::mutex> lk(m_Mutex);
if (m_Entries.size() < kCap) {
return m_Entries;
}
std::vector<LogEntry> out;
out.reserve(kCap);
out.insert(out.end(), m_Entries.begin() + static_cast<long>(m_Head),
m_Entries.end());
out.insert(out.end(), m_Entries.begin(),
m_Entries.begin() + static_cast<long>(m_Head));
return out;
}
void Clear() {
std::lock_guard<std::mutex> lk(m_Mutex);
m_Entries.clear();
m_Head = 0;
++m_Version;
}
u64 Version() const {
return m_Version.load(std::memory_order_relaxed);
}
private:
mutable std::mutex m_Mutex;
std::vector<LogEntry> m_Entries;
size_t m_Head = 0;
std::atomic<u64> m_Version{0};
std::atomic<bool> m_Enabled{false};
};
inline LogRingBuffer& LogRing() {
static LogRingBuffer inst;
return inst;
}
// spdlog sink 适配:log() 整行入环。pattern 由装配端设置(无色码)
class RingSink : public spdlog::sinks::base_sink<std::mutex> {
protected:
void sink_it_(const spdlog::details::log_msg& msg) override {
if (!LogRing().IsEnabled()) {
return; // 启动期:只落终端/文件,不进面板
}
spdlog::memory_buf_t formatted;
base_sink<std::mutex>::formatter_->format(msg, formatted);
std::string line(formatted.data(), formatted.size());
while (!line.empty() &&
(line.back() == '\n' || line.back() == '\r')) {
line.pop_back();
}
LogRing().Push(msg.level, std::move(line));
}
void flush_() override {}
};
} // namespace Engine::Debug
#endif //HELLOVULKAN_LOGRING_H
+17 -22
View File
@@ -37,29 +37,27 @@ namespace Engine
void AddAllocation(u64 bytes);
void AddDeallocation(u64 bytes);
// ---- 采样进程真实内存 ----
void SampleProcessMemory();
// ---- 打印 ----
void Print() const;
void PrintDetailed() const;
void PrintComparison() const;
// ---- 只读快照(面板/遥测消费)----
struct Snapshot
{
u64 textureCount = 0;
u64 textureBytes = 0;
u64 meshCount = 0;
u64 meshBytes = 0;
u64 bufferCount = 0;
u64 bufferBytes = 0;
u64 memoryBlockBytes = 0; // MemoryAllocator 已分配块
u64 memoryUsedBytes = 0; // 块内实际占用
u64 frameAllocation = 0; // 本帧新增(BeginFrame 清零)
u64 frameDeallocation = 0; // 本帧释放
u64 totalAllocation = 0;
u64 totalDeallocation = 0;
};
Snapshot TakeSnapshot() const;
private:
Stats() = default;
struct ProcessMemory
{
u64 VmSize = 0; // /proc/self/status VmSize
u64 Rss = 0; // /proc/self/status VmRSS
u64 Pss = 0; // smaps_rollup
u64 SharedClean = 0;
u64 PrivateDirty = 0;
u64 PrivateClean = 0;
};
ProcessMemory ReadProcessMemory() const;
mutable std::mutex m_Mutex;
// 资源计数
@@ -81,9 +79,6 @@ namespace Engine
u64 m_FrameDeallocation = 0;
u64 m_TotalAllocation = 0;
u64 m_TotalDeallocation = 0;
// 进程快照
ProcessMemory m_ProcessMem{};
};
#define STATS Engine::Stats::Get()
+68
View File
@@ -0,0 +1,68 @@
//
// Time:帧时间系统(单源采集 → 帧契约 → 静态门面)。
//
// 数据流(镜像 FrameSceneData 的落帧模式):
// Application::Run 帧首采集真实 dt(steady_clock 单源,钳制卡顿)→
// Time::BeginFrame 落定当帧 FrameTime(业务当帧只读)→ 三轨消费:
// 游戏轨 Dt() = unscaledDt × timeScale(OnUpdate/动画/粒子)
// 真实轨 UnscaledDt() (编辑器相机/UI/FPS,暂停不受影响)
// 物理轨 accumulator (吃 Dt(),scale=0 天然冻结,编辑器对账仍跑)
//
// 对齐 UE/Unity:
// Time::Delta/UnscaledDelta ≈ Unity Time.deltaTime / unscaledDeltaTime
// Time::SetTimeScale(0) ≈ Unity timeScale=0 / UE GlobalTimeDilation=0
// FrameTime.elapsed ≈ Unity Time.time(scaled 累计)
// Time::Realtime() ≈ Unity realtimeSinceStartup
// TimerManager(UE SetTimer 语义)留二期,有真实消费场景再上。
//
#ifndef HELLOVULKAN_TIME_H
#define HELLOVULKAN_TIME_H
#include <Engine/Core/Types.h>
#include <chrono>
namespace Engine {
// 帧时间契约:Run 帧首落定,当帧只读
struct FrameTime {
f64 unscaledDt = 0.0; // 真实帧时长(秒,f64 防 long-run 误差)
f32 timeScale = 1.0f; // 全局缩放(0 = 暂停)
u64 frameCount = 0;
f64 elapsed = 0.0; // 游戏累计(scaled)
f64 unscaledElapsed = 0.0; // 真实累计
f32 Dt() const { return static_cast<f32>(unscaledDt) * timeScale; }
f32 UnscaledDt() const { return static_cast<f32>(unscaledDt); }
f32 Milliseconds() const { return Dt() * 1000.0f; }
f32 UnscaledMilliseconds() const { return UnscaledDt() * 1000.0f; }
};
// 静态门面:任意深度代码免穿引用拿 Application(Unity Time 风格)
class Time {
public:
// ---- 消费端 ----
static const FrameTime& Current();
static f32 Delta(); // scaled dt(当帧)
static f32 UnscaledDelta();
static f64 Elapsed(); // scaled 累计(秒)
static f64 Realtime(); // 进程启动至今(真实秒,性能测量统一入口)
static f32 TimeScale();
static u64 FrameCount();
// ---- 控制意图(下一帧生效)----
static void SetTimeScale(f32 scale); // 0 = 暂停;负值钳为 0
// ---- 框架内部:Application::Run 帧首调用 ----
static void BeginFrame(f64 unscaledDt, u64 frameCount);
private:
static FrameTime s_Current;
static f32 s_PendingScale;
static const std::chrono::steady_clock::time_point s_Startup;
};
} // namespace Engine
#endif //HELLOVULKAN_TIME_H
+52 -2
View File
@@ -17,6 +17,16 @@ struct GLFWwindow;
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 {
public:
Window(const std::string& title, u32 width, u32 height);
@@ -33,7 +43,18 @@ namespace Engine {
using EventCallbackFn = std::function<void(const Event&)>;
void SetEventCallback(EventCallbackFn callback) { m_EventCallback = std::move(callback); }
vk::SurfaceKHR CreateSurface(vk::Instance instance);
// 输入捕获:返回 true = 该类输入被 UI 占用(如 ImGui 窗口交互)。
// 生效范围:类型化事件回调(Key/Mouse*)+ 轮询接口
//(IsKeyPressed/GetMouseDelta)。窗口类事件(Resize/Close/Focus)不受影响。
// 未设置时全部放行(行为与无 UI 时一致)。
using CaptureFn = std::function<bool()>;
void SetInputCapture(CaptureFn keyboard, CaptureFn mouse)
{
m_KeyboardCapture = std::move(keyboard);
m_MouseCapture = std::move(mouse);
}
vk::SurfaceKHR CreateSurface(vk::Instance instance) const;
void DestroySurface(vk::Instance instance, vk::SurfaceKHR surface);
u32 GetWidth() const { return m_Width; }
@@ -52,7 +73,23 @@ namespace Engine {
// 输入事件系统
bool IsKeyPressed(int key) const;
void GetMouseDelta(float& dx, float& dy);
// UI 捕获鼠标(ImGui 悬停)时返回 false——面板上的按键归 ImGui
bool IsMouseButtonPressed(int button) const;
void GetMouseDelta(float& dx, float& dy) const;
// 轮询式滚轮增量(上行 = +1,一档 ≈1):读后清零。UI 捕获鼠标
//(面板悬停)时不累积——滚轮归 ImGui
float ConsumeScrollDelta();
// 光标捕获(自由视角):DISABLED 隐藏光标出无界增量,NORMAL 恢复。
// 重复设置同值无操作;切换后鼠标增量参考点重置(防跳变)
void SetCursorCaptured(bool captured);
bool IsCursorCaptured() const { return m_CursorCaptured; }
// 手柄轮询(JOYSTICK_1 标准游戏布局):连接且带标准映射时填充
// state 并返回 true;否则置 connected=false 全零并返回 false。
// 无分配(GLFW 平台态拉取),每帧调用成本可忽略
bool PollGamepad(GamepadState& state) const;
@@ -65,6 +102,17 @@ namespace Engine {
bool m_FramebufferResized = false;
EventCallbackFn m_EventCallback;
// UI 输入捕获(ImGui 接管时置位)
CaptureFn m_KeyboardCapture;
CaptureFn m_MouseCapture;
// 光标捕获状态与鼠标增量参考点(GetMouseDelta 为 const,用 mutable)
bool m_CursorCaptured = false;
mutable bool m_FirstMouse = true;
mutable double m_LastMouseX = 0.0;
mutable double m_LastMouseY = 0.0;
mutable float m_ScrollAccum = 0.0f; // 滚轮累积(ConsumeScrollDelta)
// GLFW 静态回调(通过 window user pointer 找回 this)
static void FramebufferResizeCallback(GLFWwindow* window, int width, int height);
static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
@@ -72,6 +120,8 @@ namespace Engine {
static void CursorPosCallback(GLFWwindow* window, double xpos, double ypos);
static void ScrollCallback(GLFWwindow* window, double xoffset, double yoffset);
static void WindowFocusCallback(GLFWwindow* window, int focused);
static void WindowCloseCallback(GLFWwindow* window);
};
} // namespace Engine
+139
View File
@@ -0,0 +1,139 @@
//
// DebugDraw:引擎级通用调试图元服务(Unity Debug.DrawLine / UE
// DrawDebugHelpers 的对应物)。
//
// 分层定位:全局静态 API,任何层(脚本/物理/动画/AI)调用即收集;
// Renderer 每帧取走缓冲,PhysicsDebugPass 统一上传叠加渲染。引擎侧
// 只写这一次,之后一切新可视化 = 调用方加几行,不再碰引擎代码。
//
// 语义对齐 Unity/UE:
// duration <= 0 → 只画当前帧(调用方每帧重调,Unity DrawLine 同)
// duration > 0 → 存活 duration 秒(内部持久列表,期间无需重调;
// 固定步长系统用 2×步长 即可跨渲染帧无缝续接)
// depthTest=false → 透视遮挡直显(UE DrawDebugLine bPersistent 位)
// width → 线宽(需 wideLines 特性,设备不支持时钳回 1.0)
//
// 线程模型:主线程单写单读(脚本 OnUpdate 收集 → Renderer 上传 →
// EndFrame 翻页)。
// 预算:kMaxVerts 截断 + 一次告警(调试图元打满是滥用信号)。
//
#ifndef HELLOVULKAN_DEBUGDRAW_H
#define HELLOVULKAN_DEBUGDRAW_H
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <vector>
namespace Engine::Debug {
// 顶点(28B):与 PhysicsDebugVertex 同布局——渲染 pass 复用同一
// 顶点输入态(pos vec3 + color vec4)
struct Vertex {
glm::vec3 pos;
glm::vec4 color;
};
// 渲染批次:同 (拓扑, 线宽, 深度测试) 的连续顶点段。调用方按
// 参数自动分组,draw 侧每批一次状态切换(动态状态零管线对象)
struct Batch {
bool tris; // true = triangleList / false = lineList
f32 width;
bool depthTest;
u32 first; // 顶点偏移(顶点数,非字节)
u32 count;
};
// 单帧收集结果(Renderer 每帧消费,勿手动修改)
struct DebugFrame {
std::vector<Vertex> verts;
std::vector<Batch> batches;
void Clear() { verts.clear(); batches.clear(); }
bool Empty() const { return batches.empty(); }
};
// 顶点预算(每渲染帧):调试线框足够用,溢出截断 + 告警一次
inline constexpr u32 kMaxVerts = 1 << 16;
// ---- 全局开关 ----
void SetEnabled(bool on);
bool IsEnabled();
void Clear(); // 清空瞬时 + 持久图元(PIE 停止/场景切换时调用)
// ---- 线类(全部世界空间)----
void Line(const glm::vec3& a, const glm::vec3& b,
const glm::vec4& color,
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// Ray:from 出发沿单位方向 dirNorm 画 length 长(Unity DrawRay 同形)
void Ray(const glm::vec3& from, const glm::vec3& dirNorm, f32 length,
const glm::vec4& color,
f32 width = 1.0f, f32 duration = 0.0f, bool depthTest = true);
// 箭头:主干 + 两翼头(正交基自动构建,零向量方向安全退化成点)
void Arrow(const glm::vec3& from, const glm::vec3& to,
const glm::vec4& color, f32 headSize = 0.12f,
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// 十字标记(Unity Debug.DrawCross 同位)
void Cross(const glm::vec3& pos, f32 size, const glm::vec4& color,
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// ---- 形状线框 ----
// 三大圆经线圈(UE DrawDebugSphere 同形)
void WireSphere(const glm::vec3& center, f32 radius,
const glm::vec4& color, int segments = 16,
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// 朝向 normal 的平面圆
void WireCircle(const glm::vec3& center, f32 radius,
const glm::vec3& normal, const glm::vec4& color,
int segments = 24,
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// 轴对齐 / 旋转盒(rot 单位四元数 = AABB;UE DrawDebugBox 同形)
void WireBox(const glm::vec3& center, const glm::vec3& halfExtents,
const glm::vec4& color,
const glm::quat& rot = glm::quat(1.0f, 0.0f, 0.0f, 0.0f),
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// 胶囊线框:轴向 local +Y(Jolt 惯例),halfHeight = 圆柱段半长
//(总高 = 2×(halfHeight+radius),与引擎 CharacterController 参数一致)
void WireCapsule(const glm::vec3& center, f32 radius, f32 halfHeight,
const glm::vec4& color,
const glm::quat& rot = glm::quat(1.0f, 0.0f, 0.0f, 0.0f),
f32 width = 1.0f, f32 duration = 0.0f,
bool depthTest = true);
// ---- 便捷色:0xRRGGBB → 线性 HDR vec4(可 >1 过 tonemap 依旧醒目)----
constexpr glm::vec4 Rgb(u32 hex, f32 alpha = 1.0f)
{
constexpr f32 kInv = 1.0f / 255.0f;
return {(static_cast<f32>((hex >> 16) & 0xFF)) * kInv,
(static_cast<f32>((hex >> 8) & 0xFF)) * kInv,
(static_cast<f32>(hex & 0xFF)) * kInv,
alpha};
}
// ---- Renderer 消费端(业务代码勿调用)----
// 取当前帧缓冲(禁用/空 = Empty)。Renderer 上传窗口调用
const DebugFrame& Frame();
// 翻页:清瞬时缓冲 + 清过期持久项。Renderer 上传完成后调用
void EndFrame();
} // namespace Engine::Debug
#endif //HELLOVULKAN_DEBUGDRAW_H
+473 -24
View File
@@ -10,12 +10,20 @@
#include <Engine/Core/Types.h>
#include <Engine/Core/UUID.h>
#include <Engine/Core/AssetRef.h>
#include <Engine/Renderer/Camera.h>
#include <Engine/Assets/AssetHandle.h>
#include <Engine/Scripting/ScriptParam.h>
#include <entt/entt.hpp>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cmath>
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
namespace Engine {
class Model;
@@ -27,6 +35,8 @@ namespace Engine::ECS {
// ECS 内部遍历不用它(用 index+generation 句柄,性能)。
struct IdComponent {
UUID uuid;
operator UUID() const { return uuid; }
};
// 实体名(调试/查询用;大规模场景建议演进为哈希 id)
@@ -34,49 +44,488 @@ namespace Engine::ECS {
std::string name;
};
// 世界变换:可编辑的 SRT 分量 + 派生的世界矩阵。
// 编辑分量后调用 Compose() 重算 world;渲染热路径只读 world。
// 层级(父子变换)留给后续的层级同步系统。
// 变换(UE FTransform 语义):TRS 分量 = 相对父的 **local** 值
//(无父实体 local 即 world,旧场景零迁移)。分量私有,所有写入口
// 标脏;world 缓存(SRT 三分量 + 矩阵)由帧循环的
// Scene::UpdateTransforms() 从根 DFS 只对脏子树重算(transform-
// update 阶段)。子 world = 父 world SRT 组合而非矩阵乘法——
// 非均匀父缩放下组合式仍是干净 SRT(矩阵分解会引入剪切)。
// 渲染热路径只读 World();灯光/相机/物理/Gizmo 读 world 三分量。
//
// 旋转内部存储 = 四元数(UE FTransform 语义,不存欧拉角):
// 欧拉往返是漂移/万向锁之源(Gizmo 拖拽乱跳的根因)。欧拉只在
// 表皮出现——UI 显示(GetRotation 派生)与 UI 输入(SetRotation
// 转存);Gizmo/物理热路径走 quat 直写(SetRotationQuat)零往返。
struct TransformComponent {
glm::vec3 position{0.0f};
glm::vec3 rotation{0.0f}; // 欧拉角(度),应用顺序 Y→X→Z
glm::vec3 scale{1.0f};
glm::mat4 world{1.0f};
// ---- local 读取(相对父;UI/游戏逻辑用)----
const glm::vec3& GetPosition() const { return m_Position; }
const glm::quat& GetRotationQuat() const { return m_Rotation; }
const glm::vec3& GetScale() const { return m_Scale; }
TransformComponent& SetPosition(const glm::vec3& p) { position = p; return *this; }
TransformComponent& SetRotation(const glm::vec3& r) { rotation = r; return *this; }
TransformComponent& SetScale(const glm::vec3& s) { scale = s; return *this; }
// ---- world 缓存读取(要求已由 UpdateTransforms 计算)----
const glm::mat4& World() const { return m_World; }
const glm::vec3& GetWorldPosition() const { return m_WorldPosition; }
const glm::quat& GetWorldRotationQuat() const { return m_WorldRotation; }
const glm::vec3& GetWorldScale() const { return m_WorldScale; }
// 分量 → world 矩阵(T * R * S)
// 欧拉角(度)视图:从 quat 派生(仅 UI 显示/编辑用,勿做存储)。
// 按引擎 YXZ 序(R = Ry·Rx·Rz)从旋转矩阵反解;glm 列主序索引:
// x = asin(-R[2][1])、y = atan2(R[2][0], R[2][2])、z = atan2(R[0][1], R[1][1])。
// cx 在 atan2 分子分母中约掉,pitch 贴 ±90° 依然精确
glm::vec3 GetRotation() const
{
const glm::mat3 R = glm::mat3_cast(m_Rotation);
const f32 sp = std::clamp(-R[2][1], -1.0f, 1.0f); // = sin(pitch)
// 万向锁分支阈值 1e-6:asin 在 |sin|→1 处导数发散(sin90 的
// float 表示差 6e-8 → asin 放大成 0.02° 显示噪声,用户实测
// 90 → 89.98)。贴锁区改走组合角 atan2(分子分母同源缩放,
// 比值稳定)→ 手输 90.0 显示 90.0。落入该窄区(±0.00057°)
// 的真实 yaw/roll 被归并为组合角,UI 输入不可达,可接受
glm::vec3 euler = std::abs(sp) < 1.0f - 1e-6f
? glm::degrees(glm::vec3(
std::asin(sp),
std::atan2(R[2][0], R[2][2]),
std::atan2(R[0][1], R[1][1])))
: (sp > 0.0f
? glm::vec3(90.0f,
glm::degrees(std::atan2(-R[0][2], R[1][2])),
0.0f)
: glm::vec3(-90.0f,
glm::degrees(std::atan2(-R[0][2], -R[1][2])),
0.0f));
// 显示约定 = UE Details 的 0-360(-90 → 270)。逐轴 fmod wrap
// 与 [-180,180] 数学严格等价(每轴旋转周期 360°),矩阵重建类
// 消费端(Camera::EulerRotMat 等)不受影响;需要连续域做
// clamp/lerp 的调用方自行 wrap 回 [-180,180](如自由视角)
for (int i = 0; i < 3; ++i) {
euler[i] = std::fmod(euler[i], 360.0f);
if (euler[i] < 0.0f) euler[i] += 360.0f;
if (euler[i] >= 360.0f) euler[i] = 0.0f; // 浮点边界
}
return euler;
}
// ---- 写入(标脏,脏实体由 UpdateTransforms 重算)----
TransformComponent& SetPosition(const glm::vec3& p) { if (m_Position != p) { m_Position = p; m_Dirty = true; } return *this; }
// 欧拉角(度)入口:UI/游戏逻辑用。转 quat 存(q = qy·qx·qz,
// 与原 Ry·Rx·Rz 合成严格同构;不能用 glm::quat(euler) ctor——
// 那是 ZYX 航向序,语义不同)。输入域无限制:三角函数周期性
// 自动归一(720 ≡ -45 ≡ 315),编辑器拖动累计值直接喂
TransformComponent& SetRotation(const glm::vec3& r)
{
return SetRotationQuat(QuatFromEuler(r));
}
// 欧拉(度)→ quat 的唯一转换点(qy·qx·qz)。static 供编辑器
// mirror 的旋转等价检测复用(同一公式构造 → dot 判定严格)
static glm::quat QuatFromEuler(const glm::vec3& r)
{
const glm::quat qx = glm::angleAxis(glm::radians(r.x), glm::vec3(1, 0, 0));
const glm::quat qy = glm::angleAxis(glm::radians(r.y), glm::vec3(0, 1, 0));
const glm::quat qz = glm::angleAxis(glm::radians(r.z), glm::vec3(0, 0, 1));
return qy * qx * qz;
}
// 四元数直写(UE FTransform 语义):Gizmo 拖拽/物理写回用,
// 不经欧拉往返 → 零漂移、无万向锁
TransformComponent& SetRotationQuat(const glm::quat& q) { if (m_Rotation != q) { m_Rotation = q; m_Dirty = true; } return *this; }
TransformComponent& SetScale(const glm::vec3& s) { if (m_Scale != s) { m_Scale = s; m_Dirty = true; } return *this; }
bool IsDirty() const { return m_Dirty; }
// 显式标脏:层级系统改父/挂载后触发 world 重算用
//(local 值未变但 world 缓存已失效)
TransformComponent& MarkDirty() { m_Dirty = true; return *this; }
// SRT 分量 → world 矩阵(T * R * S),清除脏标记。
// 根实体版(world = local);由 Scene::UpdateTransforms 调用,
// 勿手动
TransformComponent& Compose()
{
glm::mat4 t = glm::translate(glm::mat4(1.0f), position);
glm::mat4 r =
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.y), glm::vec3(0, 1, 0)) *
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.x), glm::vec3(1, 0, 0)) *
glm::rotate(glm::mat4(1.0f), glm::radians(rotation.z), glm::vec3(0, 0, 1));
glm::mat4 s = glm::scale(glm::mat4(1.0f), scale);
world = t * r * s;
m_WorldPosition = m_Position;
m_WorldRotation = m_Rotation;
m_WorldScale = m_Scale;
RebuildWorldMatrix();
m_Dirty = false;
return *this;
}
// 子实体版:world = 父 world SRT 组合(UE FTransform 语义):
// wPos = pPos + pRot·(pScale⊙lPos) wRot = pRot·lRot wScl = pScale⊙lScl
// 组合式(非矩阵分解)保证非均匀父缩放下结果仍是干净 SRT。
// 要求父已 Compose(DFS 父先于子保证)。由 Scene 调用,勿手动
TransformComponent& ComposeFromParent(const TransformComponent& parent)
{
m_WorldPosition = parent.m_WorldPosition +
parent.m_WorldRotation * (parent.m_WorldScale * m_Position);
m_WorldRotation = parent.m_WorldRotation * m_Rotation;
m_WorldScale = parent.m_WorldScale * m_Scale;
RebuildWorldMatrix();
m_Dirty = false;
return *this;
}
private:
void RebuildWorldMatrix()
{
const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_WorldPosition);
const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_WorldScale);
m_World = t * glm::mat4_cast(m_WorldRotation) * s;
}
// ---- local TRS(相对父)----
glm::vec3 m_Position{0.0f};
glm::quat m_Rotation{1.0f, 0.0f, 0.0f, 0.0f}; // 单位四元数(w,x,y,z)
glm::vec3 m_Scale{1.0f};
// ---- world 缓存(UpdateTransforms 重算)----
glm::vec3 m_WorldPosition{0.0f};
glm::quat m_WorldRotation{1.0f, 0.0f, 0.0f, 0.0f};
glm::vec3 m_WorldScale{1.0f};
glm::mat4 m_World{1.0f};
bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world
};
// 层级链接(UE Actor attach):父句柄 + 子表。惰性创建——
// 只有参与父子关系的实体才有本组件,独立实体零存储成本。
// entt 句柄仅在 registry 存续期有效;序列化换 UUID,加载后重链
struct HierarchyComponent {
entt::entity parent = entt::null; // entt::null = 根实体
std::vector<entt::entity> children; // 挂载顺序
};
// 网格渲染体:引用共享的 Model 资产。
// 存句柄不存指针/Ref:所有权归 AssetManager,悬空由句柄 generation 拦截。
// 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)——
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
//
// 材质覆盖(UE Material Slot 语义):key = mesh 下标(Model::GetMeshes
// 序),value = .material 资产路径;无该项 = 用模型内建材质(只读)。
// 存路径不存句柄:覆盖材质由渲染收集期按路径惰性 resolve(资产
// 已被场景预载/编辑器指派,收集只查缓存),组件保持纯数据。
// 资产被多实体共享时编辑同时生效;实体独立 = 每实体一份覆盖文件
struct MeshRendererComponent {
Assets::ModelHandle model;
ModelRef model;
bool active = true; // 禁用 = 渲染/拾取跳过(Unity enabled / UE SetActive)
// 单 mesh 子引用:-1 = 整模型全部;N = 只画第 N 个
//(GetMeshes 下标)。物理 Mesh 碰撞体自动跟随(BuildMeshShape
// 读本组件)。序列化缺省键 = -1 向前兼容
int meshIndex = -1;
std::unordered_map<u32, std::string> materialOverrides;
};
// 骨骼动画播放状态(纯数据 + 便捷控制)——Unity Animator 组件 /
// UE SkeletalMeshComponent 动画侧的对应物。
//
// 职责分层(2026-10-06 重构,对齐 UE/Unity):
// Model(资产:node/bone/clip)→ AnimationComponent(状态,本结构)
// → Animation::AnimationSystem(调度:推进播放头 + 姿态求值 +
// palette 产出)→ SceneRenderer(渲染只读查表消费)。
// 本组件不含任何求值逻辑;ImGui 面板经 Play()/字段直接驱动。
// 无此组件 = 按绑定姿势渲染。
struct AnimationComponent {
// ---- 播放设置(序列化落盘)----
bool active = true; // 禁用 = 暂停采样(按当前帧姿态定格)
int clipIndex = 0; // 当前 clip(Model::GetClips 下标)
bool playing = true;
bool loop = true;
f32 speed = 1.0f;
// 根运动开关(Unity Animator "Apply Root Motion" 同语义):
// true = 保留根节点位移轨道(跑动 clip 自带位移会漂出锚点);
// false = 原地播放(丢弃动画子树根节点位移,旋转保留)——
// 位移归脚本/物理驱动的角色(CharacterController 等)用 false
bool applyRootMotion = true;
// ---- 播放头 / crossfade(瞬态,不落盘——重启从 0 重播)----
f32 time = 0.0f; // 当前 clip 播放头(秒,wrap 由消费端处理)
// crossfade(clip 切换平滑过渡):来源 clip 淡出期间与目标
// clip 并行采样,局部 TRS 层混合(palette 数量不变,只是求值
// 端多采一组姿态)
int fadeFrom = -1; // 来源 clip 下标(-1 = 无过渡)
f32 fadeFromTime = 0.0f; // 来源 clip 播放头
f32 fadeElapsed = 0.0f; // 已过渡时长
f32 fadeDuration = 0.3f; // 过渡总时长(秒)
// 按名播放的运行时反指(瞬态,不落盘不序列化):AnimationSystem
// 每帧回填(同 RigidBody::bodyValid 模式)。空 = 模型未就绪
const Model* runtimeModel = nullptr;
// ---- 资产引用(序列化落盘)----
// 外部动画资产(编辑器 attach 的动画容器文件,随场景持久化)。
// clips 本体运行时并入 Model 资产(共享);重启后由 Inspector
// 按此列表幂等重放
std::vector<std::string> attachedAnimPaths;
// ---- Controller 模式(Unity Animator 语义)----
// Direct = 现状单 clip 直控(clipIndex/playing 等);Controller =
// .animctrl 状态机驱动(参数 + 过渡 + BlendTree)。两模式字段
// 独立,切换互不清除(来回切配置不丢)
enum class AnimMode : u8 { Direct = 0, Controller = 1 };
AnimMode mode = AnimMode::Direct; // 落盘;缺键回退 Direct
std::string controllerPath; // .animctrl 资产路径(落盘)
// 参数实例值(落盘 = 设计意图:调好的值重启还在;缺键 = 用
// Controller 声明的默认值)。Trigger 是一次性信号不落盘
std::unordered_map<std::string, float> floatParams;
std::unordered_map<std::string, bool> boolParams;
std::vector<std::string> pendingTriggers; // 消费即清(瞬态)
// 阻尼参数推进状态(瞬态,不落盘——重启从 floatParams 当前值
// 重新起步):current 逐帧指数逼近 target,写回 floatParams 供
// 状态机条件统一读取
struct DampedParam {
float current = 0.0f;
float target = 0.0f;
float response = 8.0f; // 逼近速率(1/s,越大越快)
};
std::unordered_map<std::string, DampedParam> dampedParams;
// 状态机瞬态(不落盘,重启从 entry state 开始)
std::string currentState; // 空 = 未初始化(System 首帧置 entry)
f32 stateTime = 0.0f; // 当前状态播放头(秒)
std::string transFrom; // 过渡来源状态(空 = 无过渡)
f32 transFromTime = 0.0f; // 来源状态播放头(过渡期间继续推进)
f32 transElapsed = 0.0f; // 已过渡时长
f32 transFade = 0.25f; // 过渡总时长(秒)
// ---- 脚本/编辑器共用 API(Unity Animator.SetXxx 语义)----
void SetFloat(const std::string& name, float value)
{
floatParams[name] = value;
}
// 阻尼变体(Unity SetFloat(name, target, dampTime) 同构):登记
// 目标值,AnimationSystem Controller 驱动期逐帧指数逼近(帧率
// 无关)。首次登记从 floatParams 当前值起步(无则取目标),
// 避免跳变。与 SetFloat 同名混用 = 直接 set 会被下一帧阻尼值
// 覆盖,别混
void SetFloatDamped(const std::string& name, float target, float response)
{
auto [it, inserted] = dampedParams.try_emplace(name);
it->second.target = target;
it->second.response = glm::max(response, 0.01f);
if (inserted) {
const auto fp = floatParams.find(name);
it->second.current =
fp != floatParams.end() ? fp->second : target;
}
}
void SetBool(const std::string& name, bool value)
{
boolParams[name] = value;
}
// 一次性信号:被状态机条件评估消费后清除(同名去重——误配资产
// 无消费条件时防每帧 SetTrigger 无限堆积)
void SetTrigger(const std::string& name)
{
if (std::find(pendingTriggers.begin(), pendingTriggers.end(),
name) == pendingTriggers.end()) {
pendingTriggers.push_back(name);
}
}
// 强制切状态(CrossFade 语义:来源状态淡出、目标状态淡入,
// fade<=0 = 硬切)。编辑器下拉与脚本共用
void CrossFade(const std::string& stateName, f32 fadeSeconds)
{
if (stateName == currentState) return;
transFrom = currentState;
transFromTime = stateTime;
transElapsed = 0.0f;
transFade = glm::max(fadeSeconds, 1e-3f);
currentState = stateName;
stateTime = 0.0f;
}
// 切换 clip 并开始播放(Unity Animator.Play 语义:指派即播,
// 编辑态选中 clip 直接预览;crossfade 越界/空 clip 保护在消费端)。
// 脚本 API 与编辑器 Inspector 共用同一入口
void Play(int index, f32 fadeSeconds = 0.3f)
{
if (index == clipIndex) {
playing = true; // 重复指派 = 确保在播
return;
}
fadeFrom = clipIndex;
fadeFromTime = time;
fadeElapsed = 0.0f;
fadeDuration = fadeSeconds;
clipIndex = index;
time = 0.0f;
playing = true;
}
// 按名播放(Unity Animator.Play("Walk") 同形,推荐):clip 名经
// runtimeModel 的 clip 表解析,消掉下标硬耦合(attach 顺序变了
// 脚本不改)。未命中/模型未就绪返回 false。实现住
// AnimationSystem.cpp——需要 Model 完整类型,组件头保持零资产
// 依赖(Model.h 拖 vulkan/assimp,进本头会污染全部包含者)
bool Play(const std::string& clipName, f32 fadeSeconds = 0.3f);
};
// 相机。一个场景中 primary=true 的实体是当前渲染视角。
struct CameraComponent {
bool active = true; // 禁用 = 退出 primary 候选
Camera camera;
bool primary = false;
};
// 方向光
// 灯光。类型:
// Directional——方向即照射方向(无位置,阴影光源用第 0 盏方向光)
// Point——位置取实体 Transform(固有的),向四周发光 + 距离衰减
struct LightComponent {
glm::vec3 direction{0.0f, -1.0f, 0.0f};
glm::vec4 color{1.0f, 1.0f, 1.0f, 1.0f};
bool active = true; // 禁用 = 不进光照
enum class Type : u8 { Directional = 0, Point = 1 };
Type type = Type::Directional;
glm::vec3 direction{0.0f, -1.0f, 0.0f}; // 仅方向光
glm::vec4 color{1.0f, 1.0f, 1.0f, 1.0f}; // rgb = 颜色,a = 强度
f32 radius = 10.0f; // 仅点光:衰减半径
bool castShadow = false; // 方向光可投影(引擎取第一个 castShadow 方向光为 CSM 光源)
};
// 刚体(Jolt 后端)。纯数据 POD——头文件零 Jolt 依赖,
// bodyId 存 JPH::BodyID 的裸位模式,翻译在 PhysicsWorld。
// 生命周期:PhysicsWorld::SyncScene 每帧对账(创建/销毁/参数同步),
// 动态体的 Transform 由物理写回(Writeback 标脏),静态/运动学的
// Transform 由编辑器驱动同步进 body。
// 扩展路线:软体/布料 = SoftBodyComponent(Jolt SoftBodySharedSettings),
// 角色 = CharacterControllerComponent(CharacterVirtual),载具 = VehicleComponent
// (VehicleConstraint)——都走 PhysicsWorld 门面,不复用本组件。
struct RigidBodyComponent {
bool active = true; // 禁用 = body 销毁/不建(对账自然消化)
enum class BodyType : u8 { Static = 0, Dynamic = 1, Kinematic = 2 };
// Mesh:用实体 MeshRenderer 的视觉网格建碰撞(Unity MeshCollider
// 语义)——Static → Jolt MeshShape(精确三角网格,不可移动);
// Dynamic/Kinematic → ConvexHullShape(凸包近似,MeshShape 太贵
// 不能动)。数据源 Mesh 的 CPU 位置副本,无 MeshRenderer 回退 Box
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 即受物理驱动
ColliderType collider = ColliderType::Box;
// 碰撞体尺寸(× Transform 缩放,Unity BoxCollider.size 语义)——
// 程序化模型顶点烘焙尺寸,不能从 scale 反推。
// Capsule/Cylinder 的"半高"指圆柱段半高(不含半球端盖),总高 =
// 2*(halfHeight + radius);Jolt 两 ctor 参数序相反见 PhysicsWorld
glm::vec3 boxHalfExtents{0.5f, 0.5f, 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 gravityScale = 1.0f;
f32 friction = 0.5f;
f32 restitution = 0.2f;
// 触发器(Unity isTrigger / UE Overlap Events 语义):只产生
// Added/Removed 事件,无碰撞解算;底层 Jolt sensor
bool isTrigger = false;
// 运行时(PhysicsWorld 维护,勿手改)
u32 bodyId = 0; // JPH::BodyID 裸位
bool bodyValid = false;
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)
// 重力倍率(UE CharacterMovement.GravityScale 同位):真实 9.81
// 对人形角色偏"棉花糖"(滞空 1.2s+),游戏手感普遍 2~3 倍
f32 gravityScale = 2.0f;
// 形状附加偏移:叠加在脚底约定(胶囊底端=原点)之上,模型
// pivot 不在脚底时对齐视觉网格用(Y+ 胶囊上抬)
glm::vec3 shapeOffset{0.0f};
bool operator==(const CharacterControllerComponent&) const = default;
// 实体原点 = 脚底(胶囊底端),符合角色 pivot 习惯
};
// 脚本组件:多脚本挂载(Unity 语义——一个 GameObject 挂 N 个
// MonoBehaviour,各自有 enabled)。每项 = 脚本类名(注册表键,
// 将来 Lua/C# 同键解析)+ 独立启停位;组件级 active 是总开关。
// 实例生命周期由 ScriptRuntime 对账管理(不进组件,保持可拷贝)
struct ScriptComponent {
// 单个脚本项
struct Item {
std::string name;
bool active = true; // 禁用 = 实例保留但停 OnUpdate
// 参数(权威数据,随场景序列化)。脚本成员 = 运行时真值,
// 实例化期由 ScriptRuntime 按 schema 灌入:键缺失 → 声明
// 默认,多余键忽略;运行时 SetParam 写穿回这里(PIE 实时
// 反映,Stop 由快照天然回滚)
std::vector<Engine::Scripting::ScriptParam> params;
bool operator==(const Item&) const = default;
};
bool active = true; // 组件总开关(全部停 tick,实例保留)
std::vector<Item> items;
bool operator==(const ScriptComponent&) const = default;
};
// 天空盒/环境背景(UE Skybox Actor 语义:环境是场景里的 Actor +
// 组件,不是常驻全局设置)。envPath = 环境贴图资产(.hdr,经
// AssetManager 加载、Renderer 预积分后进 IBL/Skybox pass);空 =
// 只清屏(ClearColor 露出)。多实例取第一个 active(entt 视图序 =
// 组件创建序,首个命中即稳定)
struct SkyboxComponent {
bool active = true; // 禁用 = 不画天空(清屏色露出)
std::string envPath;
};
// 环境光(UE SkyLight 语义:全局漫射环境照明)。多实例取第一个 active
struct SkyLightComponent {
bool active = true;
glm::vec4 ambientColor{1.0f}; // 旧 Settings 默认
};
} // namespace Engine::ECS
+59 -5
View File
@@ -40,23 +40,69 @@ namespace Engine::ECS {
Scene& operator=(const Scene&) = delete;
// ---- 场景级设置 ----
// 环境类设置已迁为环境实体组件(SkyboxComponent/SkyLightComponent,
// UE Skybox/SkyLight Actor 语义);仅保留渲Target清屏色
struct Settings {
glm::vec4 ambientColor{0.15f, 0.15f, 0.2f, 0.0f};
glm::vec4 ClearColor = {0.0, 0.0, 0.0, 1.0};
};
Settings& GetSettings() { return m_Settings; }
const Settings& GetSettings() const { return m_Settings; } // 序列化只读
// ---- 实体 ----
// 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy()
EntityHandle CreateEntity(std::string name = {});
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
EntityHandle CreateEntity(std::string name = "new entity");
// 延迟:帧尾统一销毁。层级级联(UE 语义):销毁父 → 子树随葬
void DestroyEntity(Entity e);
void FlushDestroyQueue(); // 每帧末尾由 Application 调用
// 整场景换血(序列化加载):立即销毁全部实体并清空延迟队列。
// 仅用于"校验通过后的原子替换"——调用方持有的 Entity/EntityHandle
// 全部悬空,必须自行重解析(GetEntityHandle 可从裸 entity 重建)
void Clear();
// 裸 entity → 句柄表皮(EntityHandle 构造私有 + friend Scene;
// 序列化加载后应用层重解析悬空句柄用)。无效 entity 返回空句柄
EntityHandle GetEntityHandle(Entity e);
// 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update
// 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用,
// 用户代码永远不需要手动 Compose。层级语义:从根 DFS,脏实体
// 或脏祖先的后代才重算(父先于子,ComposeFromParent 组合)
void UpdateTransforms();
bool IsAlive(Entity e) const { return m_Registry.valid(e); }
// ---- 层级(UE Actor attach 语义)----
// 改父/挂根。keepWorld=true(编辑器拖拽语义):换算 local 保持
// world 位姿不变;false(序列化加载语义):local 已就位只建链。
// 防环(新父是 child 后代 → 拒绝);成功返回 true
bool SetParent(Entity child, Entity parent, bool keepWorld = true);
// 父句柄;根/悬空/无效实体返回 entt::null
Entity GetParent(Entity e) const;
// e 是否在 ancestor 的子树内(含相等)。防环检测/面板用
bool IsDescendantOf(Entity e, Entity ancestor) const;
// 世界位姿写入(UE SetActorLocation 语义):world SRT → 沿父链
// 反解 local。物理写回/Gizmo 拖拽用
void SetWorldPose(Entity e, const glm::vec3& worldPos,
const glm::quat& worldRot, const glm::vec3& worldScale);
// 沿父链自顶向下补算脏 world(层级写读 world 前调用——
// UpdateTransforms 之外的时机读取/换算 world 前保证缓存新鲜)
void EnsureWorldFresh(Entity e);
// 按稳定 UUID 查实体(序列化/跨引用场景);找不到返回 entt::null
Entity FindEntityByUUID(UUID uuid);
// 全量重建 UUID → Entity 索引(序列化加载完调一次;运行期勿调,
// CreateEntity / FlushDestroyQueue 已增量维护)
void RebuildUuidIndex();
Registry& GetRegistry() { return m_Registry; }
const Registry& GetRegistry() const { return m_Registry; } // 序列化等只读遍历
// ---- 组件便捷接口(转发 entt)----
@@ -86,13 +132,21 @@ namespace Engine::ECS {
// ---- 常用查询 ----
// primary 相机(渲染视角来源);无则返回 nullptr
CameraComponent* GetPrimaryCamera();
// 主相机(primary=true,渲染视角来源);无则返回 nullptr。
// outTransform 可选:带出该实体的 TransformComponent
//(相机位姿的单一事实源)
CameraComponent* GetPrimaryCamera(TransformComponent** outTransform = nullptr);
private:
Registry m_Registry;
std::vector<Entity> m_PendingDestroy;
Settings m_Settings;
// UUID → Entity 索引(O(1) FindEntityByUUID;SyncConstraints 每帧调用)
// CreateEntity / FlushDestroyQueue 增量维护;序列化加载后由
// RebuildUuidIndex() 全量重建一次(SceneSerializer 直接改 IdComponent.uuid
// 绕过 CreateEntity,仅靠增量会不同步)
std::unordered_map<UUID, Entity> m_UUIDToEntity;
};
} // namespace Engine::ECS
+30
View File
@@ -18,6 +18,8 @@
#pragma once
#include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h> // kMaxFramesInFlight
#include <imgui.h>
#include <vulkan/vulkan.hpp>
namespace Engine {
@@ -35,6 +37,29 @@ namespace Engine {
ImGuiLayer(const ImGuiLayer&) = delete;
ImGuiLayer& operator=(const ImGuiLayer&) = delete;
// ---- 场景显示纹理(RG PresentTarget → ImGui::Image 采样)----
// RG 物理镜像按帧槽位各一份,每槽位各注册一个描述符;
// 重编译(首次/resize)后 view 变化,Renderer 调 Update 重注册。
// 返回值直接传给 ImGui::Image(无场景纹理时为 ImTextureID_Invalid)
ImTextureID GetSceneTexture(u32 slot) const
{
return slot < kMaxFramesInFlight
? m_SceneTextures[slot] : ImTextureID_Invalid;
}
void UpdateSceneTexture(u32 slot, vk::ImageView view);
// ---- 材质球预览 atlas(MaterialPreviewPass 输出 → 材质槽 UI;
// editor 专属,HELLO_EDITOR 裁剪)----
// 注册/生命周期语义与场景纹理完全一致(按槽位各一个描述符)
#ifdef HELLO_EDITOR
ImTextureID GetPreviewTexture(u32 slot) const
{
return slot < kMaxFramesInFlight
? m_PreviewTextures[slot] : ImTextureID_Invalid;
}
void UpdatePreviewTexture(u32 slot, vk::ImageView view);
#endif
// ---- 每帧三段式(由 Application::Run 驱动)----
// 开始 UI 构建(ImGui::NewFrame)。此后到 BuildDrawData 之间,
@@ -55,8 +80,13 @@ namespace Engine {
// 生命周期保证:Application 成员序中 ImGuiLayer 在 Renderer 之后声明
//(先析构),因此 Device 引用在整个层生命周期内有效
Device& m_Device;
Window& m_Window; // 光标捕获状态查询(NewFrame hover 门控)
vk::DescriptorPool m_Pool;
bool m_Enabled = true;
ImTextureID m_SceneTextures[kMaxFramesInFlight]{};
#ifdef HELLO_EDITOR
ImTextureID m_PreviewTextures[kMaxFramesInFlight]{};
#endif
};
} // namespace Engine
+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
@@ -0,0 +1,99 @@
//
// CharacterController:单实体角色控制器(Jolt CharacterVirtual 后端)。
//
// 非刚体、扫掠运动学:CharacterVirtual 不注册进 PhysicsSystem,位置由
// 本类每步扫掠推进(撞墙滑动/斜坡限制/贴地/上台阶),其他物理对象
// "看不到"它(无碰撞解算,仅查询)。由 PhysicsWorld 对账创建/销毁并
// 在 PhysicsSystem::Update 之后驱动(Jolt 官方建议序)。
//
// 角色间碰撞(Jolt 设计行为):CharacterVirtual 彼此默认穿透,需经
// CharacterVsCharacterCollision 显式互查。PhysicsWorld 持有一份
// CharacterVsCharacterCollisionSimple 并注入每个角色(SetCharacterCollision),
// 使角色扫掠时也撞其它角色——进/出列表由本类构造/析构自动维护。
//
// 速度语义(游戏逻辑职责分工):
// - 水平移动:游戏逻辑每帧 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; // 台阶攀爬高
// 形状附加偏移(相对"胶囊底端钉实体原点"的脚底约定):
// 模型 pivot 不在脚底时对齐用(Y+ 上抬)
glm::vec3 shapeOffset{0.0f};
bool operator==(const Params&) const = default;
};
CharacterController(JPH::PhysicsSystem& system, const Params& params,
const glm::vec3& position);
// 析构:先从角色间碰撞列表摘除(防悬垂指针),Ref 随即释放
~CharacterController();
// 注入角色间碰撞列表(PhysicsWorld 单份共享):登记本角色并打开
// 互查。构造后由 PhysicsWorld 创建时调用一次;列表须比本对象活得久
void SetCharacterCollision(JPH::CharacterVsCharacterCollisionSimple& list);
// 每物理步调用:重力积分 + 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; }
// ---- 碰撞事件支持(PhysicsWorld 合成 Enter/Exit 用)----
// 本步活跃接触列表(CharacterVirtual 直通;含对方体 ID/命中点/
// 法线/距离)。CharacterVirtual 非刚体,Jolt 的 ContactListener
// 不会为它回调——由 PhysicsWorld 读本列表对比出事件边沿
const JPH::CharacterVirtual::ContactList& GetActiveContacts() const;
// 上一次 Update 扫掠"之前"的速度(撞击强度判定)。Jolt 在扫掠
// 中会抵消朝向墙面/陡面的速度分量(CancelVelocityTowardsSteepSlopes),
// 扫掠后再读 GetLinearVelocity 往往已归零——撞墙判定必须用这里
glm::vec3 GetSweepVelocity() const;
// 调试渲染:非 body,PhysicsSystem::DrawBodies 画不到,须手动
// (脚本在物理位姿/脚底原点处画胶囊线框)
void DebugDraw(JPH::DebugRenderer& renderer) const;
private:
Params m_Params;
JPH::Ref<JPH::CharacterVirtual> m_Character;
JPH::CharacterVirtual::ExtendedUpdateSettings m_Ext;
JPH::CharacterVsCharacterCollisionSimple* m_VsChar = nullptr; // 非拥有
JPH::Vec3 m_SweepVelocity{0.0f, 0.0f, 0.0f}; // 扫掠前速度快照
};
} // namespace Engine
#endif //HELLOVULKAN_CHARACTERCONTROLLER_H
@@ -0,0 +1,91 @@
//
// PhysicsDebugDraw:Jolt 调试图元收集器(JPH::DebugRendererSimple 派生)。
//
// 职责只有"收集":Jolt 各调试源(BodyManager::Draw 的形状线框/速度箭头、
// ConstraintManager::DrawConstraints、PhysicsWorld 手绘的重力/加速度箭头)
// 触发的 DrawLine/DrawTriangle 回调全部收进顶点数组;渲染侧
// (PhysicsDebugPass)每帧上传到 ring VB 绘制。收集器不理解 Vulkan,
// 渲染 pass 不理解 Jolt——图元契约见 PhysicsDebugTypes.h。
//
// 生命周期:PhysicsWorld 持有并安装为 JPH::DebugRenderer::sInstance;
// DebugRendererSimple 的构造即 DebugRenderer::Initialize()(预烘焙
// box/sphere/cylinder 等通用几何 batch),NextFrame() 每帧释放不再
// 引用的缓存几何。
//
// 扩展性(物理调试的唯一图元出口,新调试源零新通道):
// 软体布料 → DrawSettings 的 mDrawSoftBody* 开关原生生效
// 角色控制器 → CharacterVirtual::DebugDraw 走同一 sInstance
// 载具控制器 → VehicleConstraint::Draw 走同一 sInstance
//
#ifndef HELLOVULKAN_PHYSICSDEBUGDRAW_H
#define HELLOVULKAN_PHYSICSDEBUGDRAW_H
#include <Engine/Physics/PhysicsDebugTypes.h>
#ifdef JPH_DEBUG_RENDERER
#include <Jolt/Jolt.h>
#include <Jolt/Renderer/DebugRendererSimple.h>
namespace Engine {
class PhysicsDebugDraw final : public JPH::DebugRendererSimple {
public:
// 析构交给基类:~DebugRenderer 自己断言并清 sInstance(JPH_ASSERT(sInstance == this)),
// 派生类手动清会让基类断言看到 nullptr 而失败
PhysicsDebugFrame& Frame() { return m_Frame; }
// 每次收集前调用:清空上一帧图元(容量保留,稳态零堆分配)
void BeginCollect() { m_Frame.Clear(); }
// ---- DebugRenderer 回调(基类其余绘制原语全部路由到这里)----
void DrawLine(JPH::RVec3Arg inFrom, JPH::RVec3Arg inTo,
JPH::ColorArg inColor) override;
void DrawTriangle(JPH::RVec3Arg inV1, JPH::RVec3Arg inV2,
JPH::RVec3Arg inV3, JPH::ColorArg inColor,
JPH::DebugRenderer::ECastShadow inCastShadow) override;
void DrawText3D(JPH::RVec3Arg, const JPH::string_view&,
JPH::ColorArg, float) override {
// 3D 文本暂不支持(遮挡 UI 噪音,需要时再挂 ImGui 调试层)
}
private:
static glm::vec3 ToGlm(JPH::RVec3Arg v)
{
return {static_cast<f32>(v.GetX()), static_cast<f32>(v.GetY()),
static_cast<f32>(v.GetZ())};
}
static glm::vec4 ToGlm(JPH::ColorArg c)
{
constexpr f32 kInv = 1.0f / 255.0f;
return {c.r * kInv, c.g * kInv, c.b * kInv, c.a * kInv};
}
PhysicsDebugFrame m_Frame;
};
} // namespace Engine
#else // !JPH_DEBUG_RENDERER
// Distribution 构建:Jolt 调试渲染被整体裁剪(DEBUG_RENDERER_IN_* 均关),
// 收集器退化为空实现——所有消费方照常编译,零绘制零开销
namespace Engine {
class PhysicsDebugDraw {
public:
PhysicsDebugFrame& Frame() { return m_Frame; }
void BeginCollect() { m_Frame.Clear(); }
void NextFrame() {}
private:
PhysicsDebugFrame m_Frame;
};
} // namespace Engine
#endif // JPH_DEBUG_RENDERER
#endif //HELLOVULKAN_PHYSICSDEBUGDRAW_H
@@ -0,0 +1,48 @@
//
// PhysicsDebugTypes:物理调试可视化的数据契约(Jolt 零依赖)。
//
// PhysicsDebugDraw(收集器,Physics 层)生产、PhysicsDebugPass(渲染)
// 消费;渲染侧只 include 本头文件,不接触任何 Jolt 类型——分层约束
// 与 RigidBodyComponent 的"零 Jolt 依赖"一致。
//
#ifndef HELLOVULKAN_PHYSICSDEBUGTYPES_H
#define HELLOVULKAN_PHYSICSDEBUGTYPES_H
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
#include <vector>
namespace Engine {
// 图元顶点(28B):世界空间位置 + 线性 HDR 颜色(可 >1,过 tonemap
// 依旧醒目)。线段/三角形共用同一顶点格式,各走一条动态 VB
struct PhysicsDebugVertex {
glm::vec3 pos;
glm::vec4 color;
};
// 单帧收集结果:PhysicsWorld 每个 Step 调用尾按 debugBits 重新收集
//(渲染帧率,收集间隔内数据保持稳定,多渲染帧复用同一份图元)
struct PhysicsDebugFrame {
std::vector<PhysicsDebugVertex> lines; // 线段顶点(成对)
std::vector<PhysicsDebugVertex> triangles; // 三角形顶点(三张一副)
void Clear() { lines.clear(); triangles.clear(); }
bool Empty() const { return lines.empty() && triangles.empty(); }
};
// 可视化位掩码(ImGui 经 PhysicsWorld 门面读写;0 = 全关)
enum PhysicsDebugBits : u32 {
kPhyDbgColliders = 1 << 0, // collider 线框(Jolt BodyManager::Draw)
kPhyDbgConstraints = 1 << 1, // 约束(关节/软体约束,接入后生效)
kPhyDbgGravity = 1 << 2, // 重力箭头(手绘,Jolt 无现成 API)
kPhyDbgVelocity = 1 << 3, // 速度箭头(Jolt DrawSettings.mDrawVelocity)
kPhyDbgAccel = 1 << 4, // 加速度箭头(手绘,速度差分/定步长)
kPhyDbgDefault = kPhyDbgColliders | kPhyDbgGravity |
kPhyDbgVelocity | kPhyDbgAccel,
};
} // namespace Engine
#endif //HELLOVULKAN_PHYSICSDEBUGTYPES_H
@@ -0,0 +1,34 @@
//
// PhysicsEvents:碰撞事件数据契约(Jolt 零依赖)。
//
// PhysicsWorld 内部的 ContactListenerImpl 在步进中收集、帧尾入队;
// 消费方(App/游戏逻辑)只 include 本头文件,不接触任何 Jolt 类型
// ——分层约束与 RigidBodyComponent 的"零 Jolt 依赖"一致。
//
// 事件语义 = 最近一次物理步进(Step 开头清空队列,消费方每帧读取)。
// a/b 存实体位模式(entt::to_integral 的裸值),消费方用
// static_cast<entt::entity>(bits) 还原,并自行 registry.valid() 校验
// ——实体可能在事件产生后、消费前被删除。
//
#ifndef HELLOVULKAN_PHYSICSEVENTS_H
#define HELLOVULKAN_PHYSICSEVENTS_H
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
namespace Engine {
struct CollisionEvent {
enum class Kind : u8 { Added = 0, Persisted = 1, Removed = 2 };
u32 a = 0; // 实体位模式(Body1;Jolt 保证 BodyID 1 < 2)
u32 b = 0; // 实体位模式(Body2)
Kind kind = Kind::Added;
glm::vec3 point{0.0f}; // 世界空间首个接触点(Removed 时无效)
glm::vec3 normal{0.0f}; // 世界空间法线(body2 沿此方向推出;Removed 时无效)
};
} // namespace Engine
#endif //HELLOVULKAN_PHYSICSEVENTS_H
@@ -0,0 +1,28 @@
//
// PhysicsRay:射线查询数据契约(Jolt 零依赖)。
//
// PhysicsWorld::RayCast(BodyInterface/NarrowPhaseQuery 门面)生产;
// 消费方(游戏查询/编辑器拾取)只 include 本头文件,不接触任何 Jolt
// 类型——分层约束与 PhysicsEvents.h 一致。
//
#ifndef HELLOVULKAN_PHYSICSRAY_H
#define HELLOVULKAN_PHYSICSRAY_H
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
namespace Engine {
// 射线命中结果(最近命中单结果;编辑器拾取/点击选择够用,
// 穿透列表走 CastRayCollector 扩展,本契约保持最小)
struct RayHit {
u32 entityBits = 0; // 命中实体的位模式(消费方自行 valid 校验)
glm::vec3 point{0.0f}; // 世界空间命中点
glm::vec3 normal{0.0f}; // 世界空间表面法线(命中子形状)
f32 distance = 0.0f; // 命中距离(origin 起算)
};
} // namespace Engine
#endif //HELLOVULKAN_PHYSICSRAY_H
@@ -0,0 +1,230 @@
//
// PhysicsWorld:物理世界门面(Jolt 后端)。
//
// 镜像 SceneRenderer 的单向数据流:
// SyncScene(读 ECS 对账 body)→ PhysicsSystem::Update → Writeback(动态体 → Transform 标脏)
// 定步长 accumulator(1/60,最多 4 子步,积压丢弃防螺旋死亡)。
//
// 分层约束:
// - 组件层(Components.h)零 Jolt 依赖:bodyId 存 JPH::BodyID 裸位模式
// - App/编辑器只经 Application::GetPhysics() 访问开关,不接触 Jolt 头
//
// 可扩展路线(本门面是唯一入口,feature 各自分文件演进,禁止互相耦合):
// 软体/布料 → Jolt SoftBodySharedSettings/SoftBodyCreationSettings(原生支持),
// 扩展为 SyncSoftBodies/WritebackSoft(网格引用走 AssetRef)
// 角色控制器 → CharacterVirtual(运动学扫掠,非刚体,独立 Controller 类持有)
// 载具控制器 → VehicleConstraint(挂动态刚体 chassis 上,独立 Controller 类)
// 碰撞事件 → ContactListenerImpl(本类嵌套,已实现)+ GetEvents 帧尾队列
// 射线查询 → BodyInterface::RayCast 门面(编辑器拾取/游戏查询)
//
#ifndef HELLOVULKAN_PHYSICSWORLD_H
#define HELLOVULKAN_PHYSICSWORLD_H
#include <Engine/Core/Types.h>
#include <Engine/Physics/PhysicsDebugDraw.h>
#include <Engine/Physics/PhysicsEvents.h>
#include <Engine/Physics/PhysicsRay.h>
#include <Engine/Physics/CharacterController.h>
#include <Engine/ECS/Components.h> // BodyRecord 快照存 CompoundChild(完整类型)
#include <Jolt/Jolt.h>
#include <Jolt/Physics/PhysicsSystem.h>
#include <Jolt/Physics/Body/BodyID.h>
#include <Jolt/Physics/Constraints/Constraint.h> // JPH::Ref<Constraint>
#include <Jolt/Core/JobSystemThreadPool.h>
#include <Jolt/Core/TempAllocator.h>
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace Engine {
namespace ECS { class Scene; struct RigidBodyComponent; struct TransformComponent; }
namespace Assets { class AssetManager; }
class PhysicsWorld {
public:
// assets:Mesh 碰撞体解析 modelRef(MeshShape/ConvexHull 数据源)
explicit PhysicsWorld(ECS::Scene& scene, Assets::AssetManager* assets);
~PhysicsWorld();
PhysicsWorld(const PhysicsWorld&) = delete;
PhysicsWorld& operator=(const PhysicsWorld&) = delete;
// 帧循环调用(OnUpdate 之前,Unity FixedUpdate→Update 语义)
void Step(f32 dt);
void SetEnabled(bool enabled) { m_Enabled = enabled; }
bool IsEnabled() const { return m_Enabled; }
// ---- 物理调试可视化(ImGui 开关;位掩码见 PhysicsDebugBits)----
// Step 尾部按位收集图元(渲染帧率),Renderer 每帧上传绘制
void SetDebugDrawBits(u32 bits) { m_DebugBits = bits; }
u32 GetDebugDrawBits() const { return m_DebugBits; }
PhysicsDebugFrame& GetDebugFrame() { return m_DebugDraw->Frame(); }
// ---- 碰撞事件(语义 = 最近一次步进;Step 开头清空)----
// 主线程在 Step 之外读取,与多线程回调互不重叠,无需加锁
const std::vector<CollisionEvent>& GetEvents() const {
return m_PendingEvents;
}
// ---- 射线查询(游戏查询/编辑器拾取;主线程调用,暂停时也可用)----
// dir 会被内部归一化;maxDist 限制射程。命中返回 true 并填 outHit
bool RayCast(const glm::vec3& origin, const glm::vec3& dir,
f32 maxDist, RayHit& outHit) const;
// 仅命中静态(非移动层)几何:忽略碎块/动态物/NPC。相机防穿墙用它
// ——动态碎块撞进枢轴→相机射线会把镜头乱拉(抖动),静态语义才稳
bool RayCastStatic(const glm::vec3& origin, const glm::vec3& dir,
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;
// 角色脚底位置(物理权威世界坐标):调试图元/特效锚点用它,
// 而不是 Transform().GetPosition()——后者在有父级时是本地坐标
glm::vec3 GetCharacterPosition(u32 entityBits) const;
// 角色"撞击强度"速度(上一次扫掠前的速度):撞墙/碎墙等判定用它,
// 而非 GetCharacterVelocity——后者已被 Jolt 抵消掉朝向墙面的分量
glm::vec3 GetCharacterImpactVelocity(u32 entityBits) const;
private:
// 创建时缓存的参数(变更检测 → 重建 body)
struct BodyRecord {
JPH::BodyID id{};
u8 type = 0; // RigidBodyComponent::BodyType
u8 collider = 0; // RigidBodyComponent::ColliderType
glm::vec3 colliderSize{0.0f}; // 碰撞体尺寸缓存(box 半尺寸)
f32 radius = 0.0f; // 球/Capsule/Cylinder 半径(collider 决定语义)
f32 halfHeight = 0.0f; // Capsule/Cylinder 圆柱段半高
std::vector<ECS::RigidBodyComponent::CompoundChild> compound; // 子形状快照
f32 mass = 0.0f;
bool isTrigger = false; // 触发器标记(变更 → 重建 body)
glm::vec3 lastPos{0.0f}; // 静态/运动体编辑器拖动检测
glm::quat lastRot{1.0f, 0.0f, 0.0f, 0.0f}; // 同上(quat 直比,无欧拉往返)
// 加速度箭头用:上一步速度(定步长差分)与平滑后的加速度
glm::vec3 prevVel{0.0f};
glm::vec3 lastAccel{0.0f};
};
void SyncScene(); // 对账:孤儿清理 / 创建 / 参数同步
void Writeback(); // 动态体 → TransformComponent(标脏)+ lastPos/lastRot 对账基线
void UpdateAccels(f32 dt); // 速度差分 → BodyRecord.lastAccel(step 率)
void CollectDebugData(); // 按 m_DebugBits 收集调试图元(渲染帧率)
void CreateBody(u32 entityBits, ECS::RigidBodyComponent& rb,
ECS::TransformComponent& tr);
// 约束对账:owner 孤儿清理 / body 重建传染失效 / 建缺失与变更重建
// invalidated = 本帧 SyncScene 中被销毁或重建 body 的实体位模式
void SyncConstraints(const std::unordered_set<u32>& invalidated);
void CreateConstraint(u32 ownerBits, u32 targetBits,
const ECS::JointComponent& jc);
void RemoveConstraint(u32 ownerBits);
// 角色对账:孤儿清理 / 参数变更重建 / 编辑器拖动传送。
// 角色非刚体(不进 PhysicsSystem),无 body 清理负担
void SyncCharacters();
// 每物理步(PhysicsSystem::Update 之后,Jolt 官方建议序):
// 重力积分 + 扫掠 → 写回 TransformComponent(标脏)+ 合成角色
// 碰撞事件(CharacterVirtual 不进 ContactListener,见函数注释)
void UpdateCharacters(f32 dt);
// Mesh 碰撞体:实体视觉网格 → Jolt 形状(Static=MeshShape /
// Dynamic=ConvexHull)。无 MeshRenderer 或构建失败返回 null(回退 Box)
JPH::RefConst<JPH::Shape> BuildMeshShape(u32 entityBits,
ECS::RigidBodyComponent& rb,
ECS::TransformComponent& tr);
// Compound 碰撞体:子形状列表 → Jolt StaticCompoundShape。
// 空 children 或构建失败返回 null(回退 Box)
JPH::RefConst<JPH::Shape> BuildCompoundShape(ECS::RigidBodyComponent& rb,
ECS::TransformComponent& tr);
void RemoveBody(u32 entityBits);
// RayCast / RayCastStatic 共用实现:layerFilter 为空 = 命中所有层
bool RayCastImpl(const glm::vec3& origin, const glm::vec3& dir,
f32 maxDist, RayHit& outHit,
const JPH::ObjectLayerFilter* layerFilter) const;
const BodyRecord* FindRecord(u32 entityBits) const; // 刚体驱动寻址
CharacterController* FindCharacter(u32 entityBits); // 角色门面寻址
void QueueEvent(CollisionEvent&& ev); // 回调线程 append(多线程并发,加锁)
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 封装)----
// 角色间碰撞互查表:Jolt CharacterVirtual 默认彼此穿透,须显式注入
// CharacterVsCharacterCollisionSimple 才互撞。声明在 m_Characters
// 之前(析构更晚)——角色析构时会回表 Remove,列表须后于角色释放
Scope<JPH::CharacterVsCharacterCollisionSimple> m_VsCharacterCollision;
struct CharacterEntry {
std::unique_ptr<CharacterController> cc;
glm::vec3 lastPos{0.0f}; // 编辑器拖动检测
// 上一步活跃接触的"对方实体"集合:本步对比出角色碰撞
// Enter/Exit(CharacterVirtual 非刚体,ContactListener 不收)
std::unordered_set<u32> contacts;
};
std::unordered_map<u32, CharacterEntry> m_Characters;
ECS::Scene& m_Scene;
Assets::AssetManager* m_Assets = nullptr; // Mesh 碰撞体解析 modelRef
bool m_Enabled = true;
f32 m_Accumulator = 0.0f;
// 层配置实现类在 cpp;接口必须比 PhysicsSystem 活得久
Scope<JPH::BroadPhaseLayerInterface> m_BPLayerInterface;
Scope<JPH::ObjectVsBroadPhaseLayerFilter> m_ObjectVsBPLayerFilter;
Scope<JPH::ObjectLayerPairFilter> m_ObjectVsObjectLayerFilter;
Scope<JPH::TempAllocator> m_TempAllocator;
Scope<JPH::JobSystem> m_JobSystem;
Scope<JPH::PhysicsSystem> m_System;
std::unordered_map<u32, BodyRecord> m_Bodies; // entity 位模式 → body
// BodyID 序号 → entity 位模式(O(1) 反查;ContactListener 回调用)
// 与 m_Bodies 同生共死:CreateBody 同插,RemoveBody/孤儿清理同 erase。
// 主线程在 SyncScene 写、Step 内 Update 阶段不写 → 回调线程只读,无需加锁
std::unordered_map<u32, u32> m_BodyToEntity;
// ---- 碰撞事件收集(多线程回调入队,主线程消费)----
class ContactListenerImpl; // cpp 定义(JPH::ContactListener 派生)
Scope<ContactListenerImpl> m_ContactListener;
std::mutex m_EventMutex; // 保护 m_PendingEvents(回调并发)
std::vector<CollisionEvent> m_PendingEvents; // 最近一次步进的事件
// ---- 物理调试可视化 ----
// 堆上延迟构造:DebugRendererSimple 构造即预烘焙通用几何(走 JPH
// 分配器),必须等 ctor 内 call_once 注册完分配器,见 PhysicsWorld.cpp
Scope<PhysicsDebugDraw> m_DebugDraw; // Jolt 全局调试渲染后端(收集器)
u32 m_DebugBits = 0; // PhysicsDebugBits(默认关,StatsPanel 开关)
};
} // namespace Engine
#endif //HELLOVULKAN_PHYSICSWORLD_H
@@ -16,7 +16,6 @@
//
// 已知边界(后续演进点):
// - 暂不支持异步 compute queue / 跨队列信号量
// - 暂不支持内存别名(需 VMA)与帧历史资源(TAA 等)
// - 同一 pass 内对同一资源"读+写"(input attachment / RWM)未建模
//
@@ -70,8 +69,11 @@ namespace Engine::RG {
struct TextureDesc {
vk::Format format = vk::Format::eUndefined;
u32 width = 0; // 0 = 跟随目标(swapchain)尺寸
u32 width = 0; // 0 = 跟随目标(swapchain)尺寸 × sizeScale
u32 height = 0;
f32 sizeScale = 1.0f; // 相对尺寸(后处理降采样用,如 bloom 0.5)
// 附加 usage(在 pass 读写推导之外;如调试截图需要 TRANSFER_SRC)
vk::ImageUsageFlags extraUsage{};
};
// ---------- 物理镜像 ----------
@@ -96,6 +98,12 @@ namespace Engine::RG {
// 按句柄取当前帧槽位的物理资源(image/view)
vk::ImageView View(TextureHandle h) const;
vk::Image Image(TextureHandle h) const;
// 物理尺寸(导入资源 = SetImportedExtent 设定值;创建资源 = 物理镜像)
vk::Extent2D Extent(TextureHandle h) const;
// 帧历史资源:上一帧槽位的物理镜像((slot + framesInFlight - 1) % F)。
// 采样描述符经 GetPhysicalView(h, 历史槽位) 预绑(同阴影图模式)。
u32 HistorySlot(u32 frameSlot) const;
class RenderGraph* graph = nullptr;
};
@@ -123,6 +131,7 @@ namespace Engine::RG {
// 导入资源每帧绑定
vk::Image importedImage{};
vk::ImageView importedView{};
vk::Extent2D importedExtent{}; // 物理尺寸(swapchain 重建后更新)
// 帧槽位状态:物理镜像是否已被使用过(决定 entry barrier 变体)
struct SlotState {
@@ -130,6 +139,10 @@ namespace Engine::RG {
};
std::vector<SlotState> slots;
// 是否存在帧历史读(Compile 期校验:历史资源必须有同帧正常使用,
// 否则没有物理镜像可读)
bool hasHistoryUse = false;
// Compile 期状态链解算用(执行序上的"当前"状态)
vk::ImageLayout chainLayout = vk::ImageLayout::eUndefined;
vk::PipelineStageFlags2 chainStage{};
@@ -149,22 +162,31 @@ namespace Engine::RG {
u32 resIndex;
ResourceUse use;
vk::ShaderStageFlags readStage; // ShaderRead 时的消费 stage
bool history = false; // 帧历史读:读上一帧槽位的物理镜像
};
std::vector<Use> uses;
ExecuteFn exec;
bool sideEffect = false; // 无读写但必须执行(compute 写 buffer 等)
bool culled = false;
vk::Extent2D extent{}; // 本 pass 的渲染尺寸(写入附件的最大分辨率,Compile 期解析)
// Compile 期预算好的 barrier:每个资源两个变体
// steady = 该槽位已被使用过(从 resting 状态出发)
// first = 该槽位首次使用(从 Undefined 出发)
// imported 资源恒用 first(acquire 后 layout 语义上为 Undefined)
// history = 帧历史读(操作上一帧槽位的镜像,steady 变体带跨帧可见性同步)
struct BarrierPair {
u32 resIndex;
vk::ImageMemoryBarrier2 steady;
vk::ImageMemoryBarrier2 first;
bool history = false;
};
std::vector<BarrierPair> barriers;
// 历史读的出口 barrier(exec 之后执行):历史镜像被临时转进读
// layout 采样,读完必须恢复 resting layout,否则下一帧该槽位
// entry barrier 的 oldLayout 与实际 layout 脱节
std::vector<BarrierPair> postBarriers;
};
// ---------- RenderGraph ----------
@@ -188,11 +210,21 @@ namespace Engine::RG {
// 每帧 acquire 后调用,把当帧 swapchain image 绑到导入资源
void SetImportedImage(TextureHandle h, vk::Image image, vk::ImageView view);
// 导入资源的物理尺寸(swapchain 重建后同步更新)。pass 渲染区域
// 跟实际附件尺寸走:backbuffer = 窗口尺寸,与目标尺寸(场景渲染
// 分辨率)解耦——Editor 视口模式下二者不同
void SetImportedExtent(TextureHandle h, vk::Extent2D extent);
// 指定帧槽位的物理镜像 view(Compile 后有效)。
// 用途:把 RG 管理的资源绑定到外部描述符(如阴影图采样)。
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
vk::ImageView GetPhysicalView(TextureHandle h, u32 frameSlot) const;
// 帧历史槽位 = 上一帧使用的槽位:(slot - 1 + F) mod F
u32 HistorySlot(u32 frameSlot) const {
return (frameSlot + m_FramesInFlight - 1) % m_FramesInFlight;
}
// ---- Pass 声明 ----
class PassBuilder {
@@ -200,6 +232,12 @@ namespace Engine::RG {
PassBuilder& Write(TextureHandle h, ResourceUse use);
PassBuilder& Read(TextureHandle h, ResourceUse use,
vk::ShaderStageFlags stage = vk::ShaderStageFlagBits::eFragment);
// 帧历史读:读上一帧槽位的物理镜像(TAA/运动模糊/时域滤波用)。
// - 不产生图内边:跨帧依赖由同队列提交序 + 历史 barrier 保证
// - 资源必须有同帧正常使用(否则无物理镜像),Compile 期校验
// - 采样描述符在 pass 内用 GetPhysicalView(h, HistorySlot(slot)) 预绑
PassBuilder& ReadHistory(TextureHandle h, ResourceUse use,
vk::ShaderStageFlags stage = vk::ShaderStageFlagBits::eFragment);
PassBuilder& SideEffect();
private:
@@ -225,6 +263,31 @@ namespace Engine::RG {
// 每帧执行:按编译好的顺序录命令。要求 cmd 处于 recording 状态。
void Execute(const FrameContext& frame);
// GPU timestamp(可选):注入 pool 后,每个 pass 前后各写一枚
// 时间戳,索引 = frameSlot * queriesPerFrame + 执行序位置 * 2。
// pool 由调用方创建(容量 ≥ framesInFlight × queriesPerFrame),
// 读取由调用方在本槽位 fence 等待之后进行。
void SetQueryPool(vk::QueryPool pool, u32 queriesPerFrame);
// GPU profiler(可选):pass 边界的 GPU zone 钩子(Tracy 等工具的
// 接缝,RG 本体不感知任何 profiler 实现)。BeginPass 返回的 scope
// 析构 = 结束时间戳;注入者须保证 scope 生命周期覆盖 pass lambda。
// EndFrame 在全部 pass 录制完后调用(仍在当帧 cmd 内,提交前)。
class IGpuProfiler {
public:
virtual ~IGpuProfiler() = default;
class IScope {
public:
virtual ~IScope() = default;
};
[[nodiscard]] virtual Scope<IScope> BeginPass(
const char* name, vk::CommandBuffer cmd) = 0;
virtual void EndFrame(vk::CommandBuffer cmd) = 0;
};
void SetGpuProfiler(IGpuProfiler* profiler) { m_GpuProfiler = profiler; }
std::vector<std::string> GetPassNames() const;
// ---- 统计 ----
u32 GetPassCount() const;
@@ -233,6 +296,14 @@ namespace Engine::RG {
u32 GetBarrierCount() const;
u32 GetPooledImageCount() const;
// GPU timestamp:指定槽位上一轮实际写入的查询数(active pass × 2)。
// 剔除使 active 数 < 总 pass 数——读侧必须按此数取结果,按满额
// 阻塞读会等永远无人写入的查询(死锁)
u32 GetWrittenQueries(u32 frameSlot) const
{
return frameSlot < 4 ? m_WrittenQueries[frameSlot] : 0;
}
private:
friend struct RenderGraphContext;
@@ -243,7 +314,10 @@ namespace Engine::RG {
void ComputeLifetimes();
void PrecomputeBarriers();
void AllocatePhysical();
void ReleasePhysical();
// keepFixedSize = true:固定尺寸资源(desc 显式 width/height,与目标
// 渲染尺寸无关)的物理镜像不归还池、保留在 entry 上——重编译后原样
// 沿用同一 VkImage,内容存续(材质球预览 atlas 不因视口 resize 清空)
void ReleasePhysical(bool keepFixedSize);
void PurgePool(vk::Extent2D target);
PhysicalImage* AcquirePhysical(vk::Format format, vk::Extent2D extent,
vk::ImageUsageFlags usage,
@@ -274,6 +348,12 @@ namespace Engine::RG {
// 拓扑排序后的执行序(存 pass 下标)
std::vector<u32> m_ExecutionOrder;
// GPU timestamp(可选注入)
vk::QueryPool m_QueryPool{};
u32 m_QueriesPerFrame = 0;
u32 m_WrittenQueries[4] = {}; // 每槽位上一轮实际写入数(active×2)
IGpuProfiler* m_GpuProfiler = nullptr; // 非拥有(Renderer 持有)
// 物理资源池:key = (format, extent, usage),value = 空闲镜像列表
struct PoolKey {
u32 format;
+78 -29
View File
@@ -2,8 +2,20 @@
// Created by atdunbg on 2026/9/22.
//
// Camera:独立的相机模块。
// 职责:输入驱动的运动更新 + 视图/投影矩阵计算。
// 不再依赖具体渲染器——Renderer/RenderGraph 只消费它的矩阵输出。
//
// 设计(Transform 单一事实源):相机的位置与朝向完全由实体上的
// TransformComponent 承载——Update 把输入直接写回 transform,视图
// 矩阵 = 实体世界矩阵的逆(本地 -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
@@ -14,51 +26,88 @@
namespace Engine {
class Window;
namespace ECS { struct TransformComponent; }
// ---- 相机参数默认值(每实体相机参数的唯一初始来源;编辑器 UI /
// 序列化缺省回退也引用它们,改默认值一处生效)----
// 视场角(度,垂直 fov,Unity 语义;UE 默认水平 90° ≈ 垂直 45°@16:9)
inline constexpr f32 kDefaultCameraFovDeg = 45.0f;
// 裁剪面距离(米):near 主导深度精度,far 拉远到"无限"级不损近处
inline constexpr f32 kDefaultCameraNear = 0.1f;
inline constexpr f32 kDefaultCameraFar = 10000.0f;
// 正交模式半垂直高度(Unity orthographic size 语义):世界单位米
inline constexpr f32 kDefaultCameraOrthoSize = 10.0f;
class Camera {
public:
// 每帧调用:读取窗口输入,更新位置与朝向
void Update(Window& window, f32 deltaTime);
// 投影模式(UE/Unity Camera 语义:透视 = 近大远小;正交 =
// 平行投影,工程图纸/2D/等距视角)
enum class ProjectionMode : u8 { Perspective = 0, Orthographic = 1 };
glm::mat4 GetView() const;
// 输入驱动(键鼠查询统一走 Input 门面,零 Window 依赖)。
// flyRequiresRMB=true(编辑器模式):移动键仅在 RMB 按住时生效
// ——解决与 Gizmo W/E/R 工具热键的键位冲突(UE 语义:飞行时
// 键盘归相机)。全部写回传入的 transform
void Update(ECS::TransformComponent& transform,
f32 deltaTime, bool flyRequiresRMB = false);
// Vulkan 投影:内部完成 Y 翻转(clip space Y 向下)和 [0,1] 深度
glm::mat4 GetProjection(f32 aspect, f32 nearZ, f32 farZ) const;
// 焦点选中(UE frame-selection):pivot 设为 point,相机沿当前
// 视线平移对准;环绕距离沿用当前值(初始 5m,滚轮可改)
void FocusOn(ECS::TransformComponent& transform, const glm::vec3& point);
glm::vec3 GetPosition() const { return m_Position; }
void SetPosition(const glm::vec3& pos) { m_Position = pos; }
// 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose)
glm::mat4 GetView(const ECS::TransformComponent& transform) const;
void SetYaw(f32 yaw) { m_Yaw = yaw; }
void SetPitch(f32 pitch) { m_Pitch = pitch; }
f32 GetYaw() const { return m_Yaw; }
f32 GetPitch() const { return m_Pitch; }
// Vulkan 投影:内部完成 Y 翻转(clip space Y 向下)和 [0,1] 深度。
// fov/near/far/orthoSize/模式全部走成员(每实体参数),aspect 由
// 视口推导不落盘(UE/Unity 同构)
glm::mat4 GetProjection(f32 aspect) const;
// ---- 投影参数(Inspector/序列化直接读写)----
void SetFovDeg(f32 fov) { m_FovDeg = glm::clamp(fov, 1.0f, 179.0f); }
f32 GetFovDeg() const { return m_FovDeg; }
// near/far 成对写:内部保证 far > near(UI 拖动交叉时防穿透)
void SetClip(f32 nearZ, f32 farZ)
{
m_NearZ = glm::max(nearZ, 0.001f);
m_FarZ = glm::max(farZ, m_NearZ + 0.01f);
}
f32 GetNearZ() const { return m_NearZ; }
f32 GetFarZ() const { return m_FarZ; }
// 正交半垂直高度(>0;aspect 由视口推导出全宽)
void SetOrthoSize(f32 s) { m_OrthoSize = glm::max(s, 0.01f); }
f32 GetOrthoSize() const { return m_OrthoSize; }
void SetProjectionMode(ProjectionMode m) { m_ProjectionMode = m; }
ProjectionMode GetProjectionMode() const { return m_ProjectionMode; }
void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
// 鼠标视角默认关闭(需要先做 cursor capture 再打开)
void SetMouseLookEnabled(bool enabled) { m_MouseLookEnabled = enabled; }
// 相机输入总开关(默认开)。关闭后转向/环绕/推拉/飞行全部静止
void SetMouseLookEnabled(const bool enabled) { m_MouseLookEnabled = enabled; }
bool IsMouseLookEnabled() const { return m_MouseLookEnabled; }
glm::vec3 GetForward() const { return m_Forward; }
glm::vec3 GetRight() const { return m_Right; }
glm::vec3 GetUp() const { return m_Up; }
// 清除环绕 pivot(位姿被外部整体改写后 pivot 已失效——下一次
// MMB 环绕按新视线自动重建)
void ResetOrbit() { m_HasPivot = false; }
private:
void UpdateBasis();
glm::vec3 m_Position{0.0f, 0.0f, 7.0f};
f32 m_Yaw = -90.0f; // 朝向 -Z
f32 m_Pitch = 0.0f;
f32 m_MoveSpeed = 2.5f;
f32 m_MouseSensitivity = 0.1f;
bool m_MouseLookEnabled = false;
bool m_MouseLookEnabled = true;
// 缓存的方向基向量,Update 时刷新,避免每帧三角函数重复计算
glm::vec3 m_Forward{0.0f, 0.0f, -1.0f};
glm::vec3 m_Right{1.0f, 0.0f, 0.0f};
glm::vec3 m_Up{0.0f, 1.0f, 0.0f};
// ---- 投影参数(每实体;Transform 只管位姿,这里只管投影)----
f32 m_FovDeg = kDefaultCameraFovDeg;
f32 m_NearZ = kDefaultCameraNear;
f32 m_FarZ = kDefaultCameraFar;
f32 m_OrthoSize = kDefaultCameraOrthoSize;
ProjectionMode m_ProjectionMode = ProjectionMode::Perspective;
// 焦点环绕状态:pivot + 距离。首次环绕无 pivot 时自动取当前
// 视线上 m_OrbitDistance 处;FocusOn 显式设置
glm::vec3 m_OrbitPivot{0.0f};
f32 m_OrbitDistance = 5.0f;
bool m_HasPivot = false;
};
} // namespace Engine
+91
View File
@@ -0,0 +1,91 @@
//
// Created by atdunbg on 2026/9/23.
//
// Cubemap:环境立方体贴图 + IBL 预积分资源(split-sum 四件套)。
//
// 纯 GPU 资源持有类(对齐 Model 的定位):只管 image/view/采样器的
// 同生共死,不做任何生成/上传。预积分由 IblGenerator(compute 链)
// 产出后注入——环境切换的入口在 Assets 层(AssetManager::LoadEnvironment)。
//
// env cube —— 重采样 6 面(Skybox 背景)
// irradiance cube —— 半球 Lambert 积分(PBR 漫反射 ambient)
// prefilter cube —— GGX 重要性采样 × mip 分级 roughness(镜面 IBL)
// BRDF LUT —— 2D 积分表(纯数学,与环境内容无关)
// 全部 fp16,运行时采样零积分开销。
//
#ifndef HELLOVULKAN_CUBEMAP_H
#define HELLOVULKAN_CUBEMAP_H
#include <Engine/Core/Types.h>
#include <Engine/Vulkan/MemoryAllocator.h>
#include <vulkan/vulkan.hpp>
#include <vector>
namespace Engine {
class Device;
// 环境的 CPU 源数据(等距柱状 HDR 解码产物,RGBA float 行优先)。
// 纯数据无线程状态:异步加载时由后台线程产出(DecodeHdr),
// 主线程消费(FromDecoded → GPU 生成)。decode 是切换耗时大头,
// 搬出主线程后运行时切换只剩 GPU 段(~40ms)。
struct DecodedEquirect {
std::vector<f32> pixels;
u32 w = 0;
u32 h = 0;
bool Valid() const { return !pixels.empty() && w > 0 && h > 0; }
};
// GPU 纹理资源三元组(RHI 惯例):image/view/显存分配同生共死,
// 析构统一释放——杜绝"建了 image 丢了 view"类半套泄漏。
// 上传 helper 直接以此为单位产出,消费端只拿引用计数语义的整单元
struct EnvTexture {
vk::Image image{};
vk::ImageView view{};
MemoryAllocator::Allocation allocation;
};
class Cubemap {
public:
~Cubemap();
Cubemap(const Cubemap&) = delete;
Cubemap& operator=(const Cubemap&) = delete;
// ---- 消费端契约(SkyboxPass:背景;SceneGpu:PBR ambient)----
vk::ImageView GetView() const { return m_Env.view; } // 环境 cube
vk::ImageView GetIrradianceView() const { return m_Irr.view; } // 漫反射
vk::ImageView GetPrefilteredView() const { return m_Pref.view; } // 镜面
vk::ImageView GetBrdfLutView() const { return m_Lut.view; } // LUT
// cube 资源共享采样器(maxLod 覆盖 pre-filter mip 链)
vk::Sampler GetSampler() const { return m_EnvSampler; }
// LUT 专用采样器(clamp-to-edge):共享的 border 白采样器会在
// uv=1.0 处混入 50% 白色 border,导致高 roughness 镜面偏大
vk::Sampler GetBrdfLutSampler() const { return m_LutSampler; }
private:
friend class IblGenerator; // 生成端唯一构造入口
Cubemap(Device& device, MemoryAllocator& allocator,
EnvTexture env, EnvTexture irr, EnvTexture pref,
EnvTexture lut, vk::Sampler envSampler, vk::Sampler lutSampler)
: m_Device(device), m_Allocator(allocator),
m_Env(env), m_Irr(irr), m_Pref(pref), m_Lut(lut),
m_EnvSampler(envSampler), m_LutSampler(lutSampler) {}
Device& m_Device;
MemoryAllocator& m_Allocator;
EnvTexture m_Env; // 环境 cube(Skybox 背景)
EnvTexture m_Irr; // 漫反射辐照度 cube
EnvTexture m_Pref; // pre-filtered cube(mip 分 roughness)
EnvTexture m_Lut; // BRDF 积分 LUT(2D)
vk::Sampler m_EnvSampler;
vk::Sampler m_LutSampler;
};
} // namespace Engine
#endif //HELLOVULKAN_CUBEMAP_H
@@ -0,0 +1,79 @@
//
// 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 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。键鼠/光标捕获统一走 Input
void Update(f32 deltaTime)
{
m_Controller.Update(m_Transform, deltaTime,
/*flyRequiresRMB=*/true);
m_Transform.Compose(); // World() 是渲染/Gizmo 读源,随写随算
}
// 跳转(对齐视口):把相机实体(或任意实体)位姿复制过来,
// Scene view 立即从该位置观看(UE "Pilot This Camera" 的
// 一次性对齐版)。环绕 pivot 已失效,清除待 MMB 重建
void AlignTo(const ECS::TransformComponent& target)
{
// world 位姿复制(目标实体可能在层级子树下)
m_Transform.SetPosition(target.GetWorldPosition());
m_Transform.SetRotationQuat(target.GetWorldRotationQuat());
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
+75
View File
@@ -0,0 +1,75 @@
//
// Created by atdunbg on 2026/9/23.
//
// Frustum:Gribb-Hartmann 视锥平面提取 + 包围球相交测试。
//
// 平面从 view-proj 矩阵的行组合中直接抽出(无需分解参数),
// 用于按视图剔除(相机视图 / 光源视图各自独立一份)。
// 注意:near/far 平面按 Vulkan [0,1] 深度约定提取
//(z_clip ∈ [0,1]:near = row2,far = row3 - row2)——
// 因此必须传已做 [0,1] 重映射的矩阵(相机 proj / lightSpace 均是)。
// 平面在提取后归一化:包围球测试的 radius 是欧氏长度,
// 未归一化的平面距离会被透视因数放大,导致视锥边缘误剔。
//
#ifndef HELLOVULKAN_FRUSTUM_H
#define HELLOVULKAN_FRUSTUM_H
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
#include <array>
namespace Engine {
struct Frustum {
// 依次:left, right, bottom, top, near, far
// xyz = 单位法线,w = 原点到平面的有符号距离
std::array<glm::vec4, 6> planes{};
// 从 view-proj 矩阵提取 6 平面(列主序 glm 矩阵的"行"组合)。
// 必须传 [0,1] 深度重映射后的矩阵。
static Frustum FromMatrix(const glm::mat4& m)
{
Frustum f;
// 列主序下的"行 i": (m[0][i], m[1][i], m[2][i], m[3][i])
const auto row = [&m](u32 i) {
return glm::vec4(m[0][i], m[1][i], m[2][i], m[3][i]);
};
const glm::vec4 r0 = row(0);
const glm::vec4 r1 = row(1);
const glm::vec4 r2 = row(2);
const glm::vec4 r3 = row(3);
f.planes[0] = r3 + r0; // left: x' >= -w
f.planes[1] = r3 - r0; // right: x' <= w
f.planes[2] = r3 + r1; // bottom: y' >= -w
f.planes[3] = r3 - r1; // top: y' <= w
f.planes[4] = r2; // near: z' >= 0(Vulkan [0,1])
f.planes[5] = r3 - r2; // far: z' <= w
// 归一化:法线长度把透视/缩放因数从距离里除掉
for (auto& p : f.planes) {
const f32 len = glm::length(glm::vec3(p));
if (len > 1e-8f) {
p /= len;
}
}
return f;
}
// 包围球与视锥相交测试(外接球近似,略保守——宁多画不漏画)。
// 对每个平面:球心在平面负侧且距离超过 radius 才算外。
bool IntersectsSphere(const glm::vec3& center, f32 radius) const
{
for (const auto& p : planes) {
if (glm::dot(glm::vec3(p), center) + p.w < -radius) {
return false;
}
}
return true;
}
};
} // namespace Engine
#endif //HELLOVULKAN_FRUSTUM_H
@@ -0,0 +1,112 @@
//
// Created by atdunbg on 2026/9/24.
//
// IblGenerator:IBL 预积分的 compute 生成端(原 Cubemap CPU 积分路径的
// GPU 化,"动态环境切换零卡顿"第一步)。
//
// 管线:等距柱状 HDR → 上传 equirect 中间纹理(免 CPU 逐像素转换)
// → 一次性提交四段 dispatch:
// ① equirect_to_cube(硬件过滤重采样 6 面)
// ② irradiance(半球 Lambert 卷积,采样 env cube)
// ③ prefilter × mip(GGX 重要性采样,push roughness)
// ④ BRDF LUT(纯数学)
// → fence 等待 → 产出 Cubemap(四件套 fp16 资源)。
//
// 为什么不进 RenderGraph:RG 管理的是帧内 2D 瞬时资源(无 cube/数组层/
// storage 写/条件执行),而预积分是"资产加载时跑一次"的持久资源——
// 独立一次性提交与原上传路径同形态,侵入最小。残留卡顿 = fence 等待
// (毫秒级),异步 compute queue / IO 线程解码是后续演进点。
//
// 产出与消费端契约(SkyboxPass / SceneGpu set2 binding3-5)不变:
// view 形态与原 CPU 路径完全一致,绑定代码零改动。
//
#ifndef HELLOVULKAN_IBLGENERATOR_H
#define HELLOVULKAN_IBLGENERATOR_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Cubemap.h> // DecodedEquirect(解码产物数据契约)
#include <Engine/Vulkan/MemoryAllocator.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <glm/glm.hpp>
#include <vulkan/vulkan.hpp>
#include <string>
#include <vector>
namespace Engine {
class Device;
class DescriptorManager;
class Cubemap;
class IblGenerator {
public:
IblGenerator(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors, ShaderManager& shaders);
~IblGenerator();
IblGenerator(const IblGenerator&) = delete;
IblGenerator& operator=(const IblGenerator&) = delete;
// ---- 同步便捷路径(等价 DecodeHdr + FromDecoded)----
// 等距柱状 HDR → 环境四件套(失败返回 nullptr,上层走 Neutral 兜底)
Scope<Cubemap> FromHdrEquirect(const std::string& path, u32 faceSize);
// ---- 异步加载两段式(decode 是切换耗时大头,可搬后台线程)----
// 纯 CPU:HDR 文件 → 像素数据。无线程状态,worker 线程可安全调用。
static DecodedEquirect DecodeHdr(const std::string& path);
// GPU 生成(必须主线程):预解码数据 → 四件套。debugName 仅用于日志。
Scope<Cubemap> FromDecoded(const DecodedEquirect& decoded, u32 faceSize,
const std::string& debugName);
// 中性兜底:常数色环境 + 真实 BRDF LUT(描述符恒有效)
Scope<Cubemap> Neutral(const glm::vec3& rgb);
private:
// cube 兼容 image(2D array 写 + cube 采样双 view)
struct CubeTarget {
vk::Image image{};
std::vector<vk::ImageView> storageViews; // 每 mip 一个(storage 写必须单 mip)
vk::ImageView cubeView{}; // eCube 全 mip(采样端)
MemoryAllocator::Allocation allocation;
};
// 从 HDR 像素数据生成(Neutral 走同一链,仅 equirect 为 1×1 常数)
Scope<Cubemap> Generate(const f32* pixels, u32 w, u32 h,
u32 faceSize, const std::string& debugName);
CubeTarget CreateCubeTarget(u32 size, u32 mips) const;
void ReleaseStorageViews(const CubeTarget& t) const;
vk::Sampler CreateEnvSampler(u32 maxLod) const;
vk::Sampler CreateLutSampler() const;
Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_Descriptors;
ShaderManager& m_Shaders;
// set 布局:A = {0 采样输入, 1 storage 输出},B = {0 storage}
vk::DescriptorSetLayout m_IblSetLayout;
vk::DescriptorSetLayout m_LutSetLayout;
// 管线布局:无 push(equirect/irradiance/LUT)与 4B push(prefilter)
vk::PipelineLayout m_IblLayout;
vk::PipelineLayout m_IblLayoutPush;
vk::PipelineLayout m_LutLayout;
// compute shader(引用计数句柄,析构自动销毁)
ShaderManager::ShaderRef m_ToCubeShader;
ShaderManager::ShaderRef m_IrrShader;
ShaderManager::ShaderRef m_PrefShader;
ShaderManager::ShaderRef m_LutShader;
vk::Sampler m_EquirectSampler; // S=repeat(经度环绕)T=clamp
vk::CommandPool m_Pool; // 持久复用,每次生成 reset
vk::CommandBuffer m_Cmd;
vk::Fence m_Fence;
};
} // namespace Engine
#endif //HELLOVULKAN_IBLGENERATOR_H
+130 -8
View File
@@ -1,38 +1,160 @@
//
// Created by atdunbg on 2026/9/21.
//
// Material:PBR 材质 = 纹理槽组 + 因子参数 UBO + 一个描述符集。
//
// 槽位(binding = 槽位序号,与 simple_shader.frag 契约一致):
// 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性)
// 2 metallicRoughness(线性:G=粗糙度,B=金属度,glTF 约定)
// 3 emissive(自发光,sRGB) 4 ao(环境光遮蔽,线性)
// 5 roughness(独立粗糙度图,R 通道——FBX/OBJ specular 工作流,
// glTF 合图工作流不用此槽)
// 6 材质参数 UBO(fragment,kParamsBinding)
//
// 纹理非拥有(Model 的纹理缓存持有);缺槽由调用方传默认纹理。
// 因子(metallicFactor/roughnessFactor)乘在贴图上:
// glTF(MR 合图 B=金属度语义)→ 因子取 glTF 声明(默认 1/1),
// factors.z=0 → shader 直接读 MR.g 当粗糙度
// FBX(metalness 独立图 + roughness 独立图)→ metal 图落 MR 槽
// (灰度 R=G=B,.b 读金属度恰好正确),roughness 图落 Roughness
// 槽,factors.z=1 → shader 用 Roughness.r 替换 MR.g
// OBJ(specular 工作流)→ metallic=0,独立 roughness 图同 FBX;
// 无图时 roughness 因子按 Ns 换算
//
#ifndef HELLOVULKAN_MATERIAL_H
#define HELLOVULKAN_MATERIAL_H
#include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h>
#include <glm/glm.hpp>
#include <vulkan/vulkan.hpp>
#include <array>
#include <string>
namespace Engine
{
namespace Engine {
class Device;
class MemoryAllocator;
class DescriptorManager;
class Texture;
class UniformBuffer;
class Material
{
// 材质因子(乘贴图;无贴图时即最终值)
// x = metallic 因子 y = roughness 因子
// z = 独立粗糙度图开关(1 = 用 Roughness 槽 R 通道替换 MR.g)
// 注意:与 shader MaterialParams UBO 严格同序同布局(全 vec4,std140
// 无填充歧义),加字段必须双端同步(simple_shader*.frag)
struct MaterialParams {
// x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
// 默认 = 电介质表面推荐值(Unity/UE 同款):
// metallic = 0 皮肤/头发/布料等非金属的正确值
// roughness = 1.0 因子语义是"乘贴图",1.0 = 贴图值直用(同 glTF
// roughnessFactor 缺省);配合兜底 MR 纹理 G=128
// 得 effective roughness ≈ 0.5,不再是 ×0.5 后的
// 0.25 假高光(ufbx 路径不写 factors,全靠这里兜底)
glm::vec4 factors{0.0f, 1.0f, 0.0f, 0.0f};
// glTF baseColorFactor:rgb 乘进 albedo,a 作 BLEND 透明度
glm::vec4 baseColor{1.0f, 1.0f, 1.0f, 1.0f};
// glTF 表面扩展(KHR_materials_ior / transmission / volume /
// emissive_strength):x = 折射率(F0 = ((ior-1)/(ior+1))²),
// y = 透射率(0 = 普通表面),z = 自发光强度,w = 体积厚度
//(折射屏幕偏移的世界距离,0 = 无弯曲直透)
glm::vec4 surface{1.5f, 0.0f, 1.0f, 0.0f};
// 杂项语义:
// x = unlit(KHR_materials_unlit,shader 早退直出 albedo)
// y = UV 左右镜像(u → 1-u,1 = 开启)
// z = UV 上下镜像(v → 1-v,1 = 开启)
// w = 保留
// 镜像作用于材质全部纹理槽,叠加在每槽 UV 变换(uvA/uvT)之后:
// 用于纠正源资产 UV 朝向(部分角色 rip 的 UV 与 glTF 上左原点
// 约定相反,贴图整体翻转)。材质级语义,非单槽
glm::vec4 misc{0.0f, 0.0f, 0.0f, 0.0f};
// glTF emissiveFactor:自发光颜色(rgb;黑 = 不发光)。emissive
// 槽默认兜底纹理是纯黑,无贴图时 Emissive Strength 乘出来恒黑
// ——无贴图想要发光就调这里(rgb × 贴图 × surface.z)
glm::vec4 emissiveFactor{0.0f, 0.0f, 0.0f, 0.0f};
// 每槽 KHR_texture_transform(glTF2 每个 textureInfo 独立声明
// offset/scale/rotation——烘焙单份顶点 UV 无法表达"一材质多槽
// 各异变换",收敛为 shader 侧线性式 uv' = A·uv + t,作用在 glTF
// 规范 UV 空间(v=0 = 图像顶);顶点 UV 经 aiProcess_FlipUVs 已
// 归一至该空间,A/t 即规范式 off + R(rot)·S(scale)·uv 的摊平,
// 换算推导见 Model.cpp glTF 分支注释)。
// uvA[slot] = (a11, a12, a21, a22)(行主 2×2),uvT[slot] = (tx, ty, 0, 0)。
// 长度必须 = Material::kSlotCount;std140 vec4 数组步长 16B 与
// CPU 布局天然对齐
glm::vec4 uvA[6] = {{1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f},
{1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f},
{1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f}};
glm::vec4 uvT[6] = {};
// albedo 贴图 alpha 作细节叠加遮罩(脸部腮红/卧蚕等局部效果):
// rgb = 叠加色(与 baseColor 同款直乘/直用,不做空间换算),
// a = 强度(0 = 关闭,mix 恒等无副作用)。alpha 遮罩来自贴图本身,
// 无 alpha 或 alpha 恒 1 的贴图开启后即等效整体染色
glm::vec4 alphaOverlay{0.0f, 0.0f, 0.0f, 0.0f};
};
class Material {
public:
Material(Device& device,
// 槽位序号(与 binding 对齐)
enum Slot : u32 {
Albedo = 0,
Normal = 1,
MetallicRoughness = 2,
Emissive = 3,
Ao = 4,
Roughness = 5, // 独立粗糙度图(R 通道)
kSlotCount = 6, // 纹理槽数
kParamsBinding = 6, // 参数 UBO binding
};
Material(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout layout,
Texture* texture);
const std::array<Texture*, kSlotCount>& textures,
const MaterialParams& params,
bool transparent = false);
~Material();
Material(const Material&) = delete;
Material& operator=(const Material&) = delete;
vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; }
Texture* GetTexture() const { return m_Texture; }
Texture* GetTexture(Slot slot) const { return m_Textures[slot]; }
// 透明队列归属(glTF alphaMode=BLEND;CPU 收集侧用)
bool IsTransparent() const { return m_Transparent; }
// ---- 编辑器调参入口(零 GPU 重建路径)----
// 参数直写:全 in-flight 槽同步覆盖写,与 ctor 初始化同一绑定
// 语义(binding 覆盖写,ring 槽位共用同一 descriptor binding)。
// 理论上存在 in-flight 帧读到半新半旧参数的撕裂窗口(动态偏移
// ring 是根治演进点)——UI 段低频编辑 + 48B 小块 memcpy 下实践
// 无感。纹理槽变化不走这里(那是重建路径,见 MaterialAsset)。
// 实现外置(UniformBuffer 头文件内仅前置声明)
void UpdateParams(const MaterialParams& params);
// 队列归属切换(收集侧 CPU 标志,描述符不变):透明 ↔ 不透明
void SetTransparent(bool t) { m_Transparent = t; }
// 材质缓存键:同贴图组合+同因子+同透明语义共享一份(Model 层使用)
static std::string MakeCacheKey(const std::string paths[kSlotCount],
const MaterialParams& params,
bool transparent);
// ---- 预览缩略图身份(editor tile 池的缓存键,game 不消费)----
// serial:出生唯一号(进程内单调递增,不复用)——裸指针做键有
// ABA 风险(旧材质析构后新材质恰好分配到同一地址,池误判命中)
u64 GetSerial() const { return m_Serial; }
// editVersion:参数编辑代数,UpdateParams 每次 ++
//(tile 池以此判定"缓存的球已过期需重渲")
u32 GetEditVersion() const { return m_EditVersion; }
private:
Texture* m_Texture = nullptr;
std::array<Texture*, kSlotCount> m_Textures{};
vk::DescriptorSet m_DescriptorSet;
Scope<UniformBuffer> m_ParamsBuffer; // per-slot 参数 ring(创建时写死)
bool m_Transparent = false;
u64 m_Serial = 0; // ctor 取号(定义见 Material.cpp)
u32 m_EditVersion = 0;
};
} // namespace Engine
@@ -0,0 +1,93 @@
//
// Created by atdunbg on 2026/10/5.
//
// MaterialAsset:独立材质资产(.material 文件)+ GPU 资源封装。
//
// UE UMaterial 的资产化对应物。语义约定:
// - 模型内建材质(Model 材质缓存持有,随 glTF/FBX 解析生成)只读,
// 不存在"原地编辑"路径——改它会波及所有引用该模型的实体
// - 可编辑材质 = .material 资产文件,被 MeshRenderer 的覆盖表
// (mesh 下标 → 资产路径)按实体引用;资产被多实体共享时编辑
// 同时生效(资产级语义),要实体独立就每实体一份文件
// - 数据段(MaterialAssetData,磁盘 ↔ 内存纯值)与 GPU 段(纹理 +
// Material)分离:GPU 段由 VulkanAssetUploader 构建,资产层与
// 本类都不感知 Vulkan——依赖方向与 Model/资产层完全一致
//
// 扩展点(UV 动效/特效等后续迭代):
// - 数据段加字段 + Version 递增,Load 全部 as<T>(default) 缺省
// 兼容——旧文件永远可读,新字段对旧文件取默认值
// - GPU 段参数经 Material::UpdateParams 直写 UBO,shader 侧
// matParams 块同步扩展即可,本类结构不变
//
#ifndef HELLOVULKAN_MATERIALASSET_H
#define HELLOVULKAN_MATERIALASSET_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Material.h>
// Texture 完整类型(非前置声明):成员持 Scope<Texture> 数组,若仅
// 前置声明,所有实例化本类析构/构造的 TU 都需各自看到 Texture 完整
// 类型——直接包含一劳永逸。Material.h 已含 vulkan.hpp,无新增重依赖
#include <Engine/Vulkan/Texture.h>
#include <array>
#include <string>
namespace Engine {
class Device;
class MemoryAllocator;
// 材质槽位默认纹理工厂(单点实现防漂移):Model 兜底纹理与
// MaterialAsset 构建共用同一组 1x1 兜底语义
//(albedo 白 / normal 平 / MR 中性 / emissive 黑 / AO 全遮蔽 / rough 0.5)
Scope<Texture> CreateDefaultMaterialTexture(
Device& device, MemoryAllocator& allocator, Material::Slot slot);
// 材质资产数据段(磁盘格式 ↔ 内存值,无 GPU 状态)。
// texPaths = 文件系统相对 CWD 路径(与模型解析产物同源,glTF/FBX
// 纹理可直接快照);空串 = 该槽用共享默认纹理兜底。
// glb 内嵌纹理在模型导入期已抽到 <模型目录>/textures/ 落盘并回写为
// 真实路径(Model::ParseFromDisk),故此处与文件纹理同语义——覆盖
// 材质可正常继承
struct MaterialAssetData {
std::array<std::string, Material::kSlotCount> texPaths;
MaterialParams params;
bool transparent = false;
bool operator==(const MaterialAssetData&) const = default;
};
class MaterialAsset {
public:
static constexpr u32 kVersion = 1;
// ---- 磁盘序列化(.material,YAML)----
// 失败(无法打开/解析)返回 false;Load 缺省键兼容
static bool Save(const std::string& path, const MaterialAssetData& data);
static bool Load(const std::string& path, MaterialAssetData& out);
// GPU 段注入(仅 VulkanAssetUploader 调用):material = 组装好的
// 材质(空槽已由 uploader 绑共享默认纹理)。纹理所有权归 uploader
// 的路径缓存(跨材质去重),本资产只持有绑进 Material 的裸指针
MaterialAsset(MaterialAssetData&& data, Scope<Material>&& material);
// 析构外置(成员为重资源句柄,销毁序与 GPU 生命周期解耦遵循
// Model/Material 既有模式)
~MaterialAsset();
const MaterialAssetData& GetData() const { return m_Data; }
Material* GetMaterial() const { return m_Material.get(); }
// ---- 编辑入口(Inspector 调;数据段与 GPU 段同步更新)----
// 写盘时机由编辑器决定(参数松手落盘,纹理变化走资产重建)
void EditParams(const MaterialParams& params);
void SetTransparent(bool t);
private:
MaterialAssetData m_Data;
Scope<Material> m_Material;
};
} // namespace Engine
#endif //HELLOVULKAN_MATERIALASSET_H
+59 -7
View File
@@ -8,6 +8,7 @@
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Vertex.h>
#include <Engine/Vulkan/Buffer.h>
#include <vulkan/vulkan.hpp>
#include <vector>
@@ -18,28 +19,79 @@ namespace Engine
class Buffer;
class Material;
// LOD 级数:LOD0 = 原始网格,越高越简(解析期 meshopt_simplify 生成,
// 共享顶点数据,只换索引区间)
inline constexpr u32 kMeshLodCount = 3;
class Mesh
{
public:
// material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份)
// lods[0] 必须为原始索引(与 vertices 对应),各级简化链由解析期生成
// cpuPositions 非空(蒙皮 mesh)= 物理数据源与包围球改用它
//(绑定姿态渲染空间,与视觉同源;vertices.position 是蒙皮原始空间)
Mesh(Device& device, MemoryAllocator& allocator,
const std::vector<Vertex>& vertices,
const std::vector<u32>& indices,
Scope<Material> material);
const std::vector<std::vector<u32>>& lods,
Material* material,
const std::vector<glm::vec3>* cpuPositions = nullptr);
~Mesh();
Mesh(const Mesh&) = delete;
Mesh& operator=(const Mesh&) = delete;
void Draw(vk::CommandBuffer cmd) const;
// instanceCount/firstInstance:GPUDriven 按 mesh 分 batch 后,
// 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取。
// lod:索引区间选择(越界取最高级)
void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1,
u32 firstInstance = 0, u32 lod = 0) const;
Material* GetMaterial() const { return m_Material.get(); }
u32 GetIndexCount() const { return m_IndexCount; }
Material* GetMaterial() const { return m_Material; }
u32 GetLodIndexCount(u32 lod) const
{
return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].indexCount;
}
// 全局连续槽位(SceneRenderer 数组化 batch 查找用,避免逐实例 hash)
u32 GetSlot() const { return m_Slot; }
// ---- MDI 兼容取数(per-mesh buffer:偏移恒 0,firstIndex 即全局)----
vk::Buffer GetVertexBuffer() const { return m_VertexBuffer->Get(); }
vk::Buffer GetIndexBuffer() const { return m_IndexBuffer->Get(); }
u32 GetVertexOffset() const { return 0; }
u32 GetGlobalFirstIndex(u32 lod) const
{
return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].firstIndex;
}
// 局部空间包围球(构造时从顶点算出),视锥剔除用
struct Bounds {
glm::vec3 center{0.0f};
f32 radius = 0.0f;
};
const Bounds& GetBounds() const { return m_Bounds; }
// ---- CPU 位置副本(物理 MeshShape/ConvexHull 数据源)----
// GPU 上传后完整 Vertex 即弃,但物理碰撞仍需三角形:构造时抽
// 位置-only(12B/顶点,约为完整 Vertex 的 1/5)+ LOD0 索引。
const std::vector<glm::vec3>& GetCpuPositions() const { return m_CpuPositions; }
const std::vector<u32>& GetCpuIndices() const { return m_CpuIndices; }
private:
// 物理碰撞数据源(MeshShape 三角网格 / ConvexHull 点云)
std::vector<glm::vec3> m_CpuPositions;
std::vector<u32> m_CpuIndices; // LOD0(原始精度)
// 同一 index buffer 内连续排布的各级索引区间
struct LodRange {
u32 firstIndex = 0;
u32 indexCount = 0;
};
Scope<Buffer> m_VertexBuffer;
Scope<Buffer> m_IndexBuffer;
Scope<Material> m_Material;
u32 m_IndexCount = 0;
std::array<LodRange, kMeshLodCount> m_Lods{};
Material* m_Material;
u32 m_Slot = 0;
Bounds m_Bounds;
u64 m_VertexBytes = 0;
u64 m_IndexBytes = 0;
@@ -0,0 +1,72 @@
//
// Created by atdunbg on 2026/10/18.
//
// MeshFactory:预制图元的 CPU 几何生成端(唯一几何源)。
//
// 定位对齐 UE 的 Engine\Source\Runtime\Engine\...(内置基础网格)思路:
// 引擎只保留一套"参数化图元 → 顶点/索引"的生成器,程序化 Model、
// 物理形状、脚本与编辑器下拉全部复用它,避免各路径各写一份立方体。
//
// 契约:
// - 所有图元中心在局部原点(平面在 y=0 的 XZ 面),尺寸为世界单位,
// 尺寸变化统一走实体 Transform 缩放,不按尺寸炸资产缓存;
// - 面法线朝外、CCW 从外正视,可被背面剔除;
// - 切线已按 UV 正交化(法线贴图 TBN 可用),退化为任一与 N 正交向量;
// - 输出 LOD0 单级索引(图元顶点少,无简化链价值)。
//
#ifndef HELLOVULKAN_MESHFACTORY_H
#define HELLOVULKAN_MESHFACTORY_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Vertex.h>
#include <string>
#include <vector>
namespace Engine {
// 图元参数(形状无关的标量集):各形状只读取自己关心的字段。
// 默认值给"半径/半边长 0.5、高 1"的规格化尺寸,贴合单位立方体约定。
struct PrimitiveDesc {
f32 size = 0.5f; // 半边长(cube/plane)/ 半径(sphere/cylinder/cone)
f32 height = 1.0f; // 圆柱/圆锥高(沿 +Y,几何中心在原点)
f32 majorRadius = 0.5f; // 圆环主半径
f32 minorRadius = 0.15f; // 圆环管半径
u32 segments = 32; // 环绕细分(球/柱/锥/圆环主环)
u32 rings = 16; // 纵向细分(球)/ 圆环管环细分
u32 subdivisions = 1; // 平面每轴网格细分
};
// 预制图元的 CPU 几何(渲染 Mesh 与物理/脚本共用)。
struct MeshData {
std::vector<Vertex> vertices;
std::vector<u32> indices; // LOD0(三角形列表)
};
class MeshFactory {
public:
// 按名字取形状(ShapeNames 中任一);未知名字返回空 MeshData。
static MeshData Make(const std::string& shape, const PrimitiveDesc& desc);
// ---- 具名构造(编辑器下拉 / 物理形状 / 脚本可直接调用)----
static MeshData Cube(f32 halfSize);
static MeshData Plane(f32 halfSize, u32 subdivisions = 1);
static MeshData Sphere(f32 radius, u32 segments = 32, u32 rings = 16);
static MeshData Cylinder(f32 radius, f32 height, u32 segments = 32);
static MeshData Cone(f32 radius, f32 height, u32 segments = 32);
static MeshData Torus(f32 majorRadius, f32 minorRadius,
u32 majorSegments = 32, u32 minorSegments = 16);
// 已知图元名(UI 下拉 / 伪路径校验的唯一定义点)
static const std::vector<std::string>& ShapeNames();
static bool IsKnownShape(const std::string& shape);
private:
// 逐三角面积加权累积切线 → 对 N 正交化 → 归一(退化取正交兜底)
static void ComputeTangents(MeshData& mesh);
};
} // namespace Engine
#endif //HELLOVULKAN_MESHFACTORY_H
+262 -11
View File
@@ -8,9 +8,15 @@
#pragma once
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Material.h>
#include <Engine/Renderer/Mesh.h>
#include <Engine/Renderer/MeshFactory.h> // 程序化几何唯一源(MeshData/PrimitiveDesc)
#include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp>
#include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用)
#include <algorithm>
#include <array>
#include <filesystem>
#include <string>
#include <unordered_map>
#include <vector>
@@ -25,22 +31,161 @@ namespace Engine
class MemoryAllocator;
class DescriptorManager;
class Texture;
class Material;
// ---- CPU 解析产物(异步加载的跨线程交棒数据)----
// 纯数据:几何 + 材质槽位引用(路径 + 因子),无任何 GPU 资源。
// worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。
struct ParsedMesh {
std::vector<Vertex> vertices;
// LOD 链([0] = 原始索引,越往后越简):解析期 meshopt_simplify
// 生成(worker 线程),消费端共享顶点只换索引
std::vector<std::vector<u32>> lods;
std::array<std::string, Material::kSlotCount> texPaths;
MaterialParams params;
bool transparent = false; // glTF alphaMode=BLEND(透明绘制队列)
bool skinned = false; // 有骨骼(渲染走 palette 蒙皮)
std::string name; // UI 名字:材质名(AI_MATKEY_NAME)优先,
// 回退 aiMesh mName(可空 → UI 回退 Slot N)
// 蒙皮 mesh 专属:绑定姿态渲染空间的位置副本(palette(bind) 按
// 顶点权重混合,与 shader 同式)。非空 = Mesh 的物理数据源与
// 包围球改用它——FBX 单位换算折在骨骼链上(bind palette ≠ I),
// 直接用原始顶点会让物理凸包大一个数量级(M1911 实测 10×)
std::vector<glm::vec3> cpuPositions;
};
// 内建材质源信息快照(per-mesh,与 GetMeshes() 下标对齐)。
// 只读语义:这是"模型固有材质"的权威描述——编辑器用它展示内建
// 材质并生成覆盖材质快照(Create Override),**永不回写**:
// 内建材质属于模型资产,原地修改会波及所有引用该模型的实体。
// 可编辑材质走 MaterialAsset(.material 文件)+ 实体级覆盖表
struct MeshMaterialInfo {
std::string name; // UI 名字:材质名优先,回退 mesh 名(空 → Slot N)
std::array<std::string, Material::kSlotCount> texPaths;
MaterialParams params;
bool transparent = false;
};
// ---- 骨骼动画数据(assimp aiNode/aiAnimation/aiBone 的引擎侧镜像)----
// 关键帧(time 单位 = clip 原生 tick,运行时按 tps 换算秒)
struct PosKey { f32 time; glm::vec3 value; };
struct RotKey { f32 time; glm::quat value; };
struct SclKey { f32 time; glm::vec3 value; };
// 一条节点动画轨道:作用于 node 表中下标为 nodeIdx 的节点;
// 无轨道的分量运行时用节点静止姿态(localTransform)
struct AnimChannel {
u32 nodeIdx = 0;
std::vector<PosKey> positions;
std::vector<RotKey> rotations;
std::vector<SclKey> scales;
};
struct AnimationClip {
std::string name;
f32 duration = 0.0f; // 秒(已按 tps 换算)
f32 tps = 1.0f; // ticks per second(0 按 1,assimp 惯例)
std::vector<AnimChannel> channels;
// 外部动画文件来源路径(运行时并入的 clip 标记;空 = 模型内嵌)。
// 用于幂等去重:同一源文件不重复并入
std::string sourcePath;
};
// aiNode 树拍平表:骨骼 global 变换的累乘骨架(解析期 ProcessNode
// 原本丢弃 mTransformation,骨骼层级必须保留)
struct ModelNode {
std::string name;
i32 parent = -1; // 根节点 = -1
glm::mat4 localTransform{1.0f}; // 静止姿态 local TRS(assimp 行主→glm 列主已转置)
};
// 骨骼 = palette 槽位 ↔ 节点 ↔ 逆绑定矩阵
// palette[i] = global(bone.nodeIdx) × invBind
struct ModelBone {
std::string name;
u32 nodeIdx = 0;
glm::mat4 invBind{1.0f}; // aiBone mOffsetMatrix(绑定姿势全局逆)
};
struct ParsedModel {
std::string path;
bool isGltf = false;
std::vector<ParsedMesh> meshes;
std::vector<ModelNode> nodes;
std::vector<ModelBone> bones;
std::vector<AnimationClip> clips;
// GLB 内嵌贴图(assimp "*<index>" 约定):解析期从 aiScene 拷出
// 原始编码字节(PNG/JPEG),实例期 GetOrLoadTexture 解码上传。
// 解析线程无 GPU 上下文,只带数据不建纹理
std::unordered_map<std::string, std::vector<u8>> embeddedImages;
// 解析有效性:mesh 与 clip 至少其一非空。纯动画 FBX(无网格只有
// 骨骼动画,UE 的 Animation Sequence 资产同理)是合法资产——
// 过去只认 meshes 导致动画源文件在缓存层被整体拒载
bool Valid() const { return !meshes.empty() || !clips.empty(); }
};
class Model
{
public:
// 同步便捷路径(等价 ParseFromDisk + FromParsed,启动期小模型可用)
Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
const std::string& path);
// 程序化模型:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
// size 为半边长(平面总边长 = 2*size)。
// ---- 异步加载两段式(assimp 解析占大模型耗时大头,可搬后台线程)----
// 纯 CPU:读盘 + assimp 解析 → 几何 + 材质引用。无线程状态。
static ParsedModel ParseFromDisk(const std::string& path);
// 资产显示名(编辑器 UI / clip 命名的唯一来源):取文件 stem;
// stem 无信息量(空 / 导入资产库的 "asset")时读同目录 pathname
// 文件取原始资产 basename,再回退父目录名。避免 UI 直接显示
// 哈希路径(用户看到的是 id)
static std::string AssetDisplayName(const std::string& path);
// GPU 构建(必须主线程):纹理上传 / mesh 缓冲 / 材质描述符。
// 解析失败返回 nullptr。
static Scope<Model> FromParsed(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
ParsedModel&& parsed);
// 程序化几何标签:几何统一来自 MeshFactory(单一几何源),
// 本类只负责把几何 + 纯色 albedo 组装成可渲染 Model
struct ProceduralTag {};
// 程序化纯色平面:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
// size 为半边长(平面总边长 = 2*size)。
static Scope<Model> CreatePlane(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b);
// 半透明纯色平面(alpha 0-255,预乘 alpha 链):透明管线验证用
static Scope<Model> CreateTransparentPlane(
Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b, u8 a);
// 程序化立方体(halfSize = 半边长,几何走 MeshFactory::Cube):
// 通用占位几何(碎片模拟的墙/块等)。尺寸变化走实体 Transform
// 缩放——同 halfSize 同色共享一份缓存模型
static Scope<Model> CreateCube(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b);
// 通用程序化图元(MeshFactory 形状名:cube/plane/sphere/cylinder/
// cone/torus 等):几何由 MeshFactory::Make 生成,纯色 albedo。
// 非法形状名返回 nullptr(调用方回退)
static Scope<Model> CreatePrimitive(Device& device,
MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
const std::string& shape,
const PrimitiveDesc& desc,
u8 r, u8 g, u8 b);
~Model();
@@ -48,30 +193,136 @@ namespace Engine
Model& operator=(const Model&) = delete;
const std::vector<Scope<Mesh>>& GetMeshes() const { return m_Meshes; }
// 内建材质源信息(与 GetMeshes() 下标对齐;只读,见结构注释)
const std::vector<MeshMaterialInfo>& GetMeshMaterialInfos() const
{
return m_MeshMaterialInfos;
}
// 全模型联合包围球(各 mesh bounds 的并集,局部空间):
// 实体级粗剔除用——整个模型在视锥外直接跳过全部 mesh 循环
const Mesh::Bounds& GetBounds() const { return m_Bounds; }
// ---- 骨骼动画运行时数据(无动画模型全部为空,零开销)----
bool HasAnimations() const { return !m_Clips.empty(); }
u32 GetBoneCount() const { return static_cast<u32>(m_Bones.size()); }
const std::vector<AnimationClip>& GetClips() const { return m_Clips; }
// 外部动画并入(编辑器 attach 操作):追加到 clip 表尾部,
// 既有 clipIndex 不受影响。仅运行时内存状态(重启后由场景
// 里 Animation.attachedAnimPaths 驱动重放)
void AppendClips(std::vector<AnimationClip>&& extra)
{
m_Clips.insert(m_Clips.end(),
std::make_move_iterator(extra.begin()),
std::make_move_iterator(extra.end()));
}
// 外部动画剥离(编辑器 detach 操作):删除 sourcePath 匹配的
// clip,返回删除数。clipIndex/fadeFrom 下标悬空由消费端自愈
//(AnimationSystem 每帧 clamp)
u32 RemoveClipsBySource(const std::string& sourcePath)
{
const u32 before = static_cast<u32>(m_Clips.size());
m_Clips.erase(
std::remove_if(m_Clips.begin(), m_Clips.end(),
[&](const AnimationClip& c)
{ return c.sourcePath == sourcePath; }),
m_Clips.end());
return before - static_cast<u32>(m_Clips.size());
}
const std::vector<ModelNode>& GetNodes() const { return m_Nodes; }
const std::vector<ModelBone>& GetBones() const { return m_Bones; }
private:
// 程序化几何骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 顶点/索引
// 由 MeshFactory 产出(见 .cpp)。name = 编辑器材质槽显示名。
// 各具名工厂(CreatePlane/Cube/Primitive)都折到这一个 ctor
Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
ProceduralTag, f32 halfSize, const std::array<u8, 3>& color);
ProceduralTag, MeshData&& geo, const std::array<u8, 4>& rgba,
bool transparent, const std::string& name);
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout);
void LoadModel(const std::string& path);
void ProcessNode(aiNode* node, const aiScene* scene);
Scope<Mesh> ProcessMesh(aiMesh* mesh, const aiScene* scene);
// aiNode 树拍平成 node 表(DFS 序,parent 下标回填)
static u32 FlattenNodes(const aiNode* node, i32 parent,
std::vector<ModelNode>& out);
// 每帧动画的 channel 轨道提取(nodeName → node 表下标)
static void ParseAnimations(const aiScene* scene,
const std::vector<ModelNode>& nodes,
std::vector<AnimationClip>& out);
static void ProcessNode(const aiNode* node, const aiScene* scene,
std::vector<ParsedMesh>& out,
const std::filesystem::path& modelDir,
bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
std::unordered_map<std::string,
std::vector<u8>>& embeddedImages,
const aiMatrix4x4& parentGlobal
= aiMatrix4x4());
static ParsedMesh ProcessMesh(const aiMesh* mesh, const aiScene* scene,
const std::filesystem::path& modelDir,
bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
std::unordered_map<std::string,
std::vector<u8>>& embeddedImages,
const aiMatrix4x4& globalTransform
= aiMatrix4x4());
// mesh 的 aiBone 权重灌进顶点 + 模型级骨骼表注册去重
static void CollectMeshBones(const aiMesh* mesh,
std::vector<Vertex>& vertices,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots);
void InitDefaultTextures();
Texture* GetOrLoadTexture(const std::string& path);
// GLB 内嵌贴图字节("*N" → 编码数据):解析期拷出,纹理解析期
// GetOrLoadTexture 消费(随实例生命周期,键同 texPaths 引用)
std::unordered_map<std::string, std::vector<u8>> m_EmbeddedImages;
// 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除)
void RebuildUnionBounds();
Texture* GetOrLoadTexture(const std::string& path, bool srgb);
// 材质缓存入口:同贴图组合+同因子+同透明语义共享一份 Material,
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
// paths 下标 = Material::Slot;params = metallic/roughness 因子;
// slotOverrides = 程序化模型的槽位纹理直供(非空项优先于路径/默认)
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params,
bool transparent = false,
const std::array<Texture*, Material::kSlotCount>&
slotOverrides = {});
Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_DescriptorManager;
vk::DescriptorSetLayout m_TextureLayout;
std::vector<Scope<Mesh>> m_Meshes;
std::string m_ModelPath;
// 纹理缓存:同一路径只加载一次
// 缓存声明在 m_Meshes 之前:mesh 持 Material* 裸指针,
// 析构序 = meshes 先于材质/纹理缓存
std::unordered_map<std::string, Scope<Material>> m_MaterialCache;
std::unordered_map<std::string, Scope<Texture>> m_TextureCache;
Scope<Texture> m_DefaultTexture;
// 各槽位默认纹理(材质缺图时兜底):白 / 平法线 / 黑高光 / 黑自发光
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures;
std::vector<Scope<Mesh>> m_Meshes;
Mesh::Bounds m_Bounds; // mesh 建完后聚合(实体级粗剔除)
// 与 m_Meshes 平行的内建材质快照(每条建 mesh 路径同步填充:
// FromParsed / 同步 LoadModel / 程序化平面)
std::vector<MeshMaterialInfo> m_MeshMaterialInfos;
std::string m_ModelPath;
bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV
// ---- 骨骼动画运行时数据(FromParsed/LoadModel 从解析产物 move)----
std::vector<ModelNode> m_Nodes;
std::vector<ModelBone> m_Bones;
std::vector<AnimationClip> m_Clips;
};
} // namespace Engine
@@ -0,0 +1,72 @@
//
// Created by atdunbg on 2026/9/29.
//
// BlitPass:显示模式适配器(kDisplayDirect 专用)。
// Direct 模式:vkCmdBlitImage 把 PresentTarget(tonemap 输出)拷进
// backbuffer,ImGui 随后 loadOp=Load 叠加 —— 场景铺满窗口的游戏形态。
// Viewport(Editor)模式:空转,backbuffer 由 ImGui 清屏独占,场景
// 经 ImGui::Image 呈现。
//
// 用 vkCmdBlitImage 而非着色器:同格式全屏拷贝零 shader 成本(声明
// TransferSrc/TransferDst use,layout/barrier 由 RG 推导)。Blit 不在
// rendering scope 内,直接录在 cmd 上,合法。
//
// 注:Blit 只在 Direct 模式运行,此时 PresentTarget 与 backbuffer 同为
// 目标尺寸(1:1),src/dst offset 均取本 pass 渲染区域即可。
//
#ifndef HELLOVULKAN_BLITPASS_H
#define HELLOVULKAN_BLITPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <array>
namespace Engine::Passes {
class BlitPass final : public RenderPass {
public:
BlitPass(FrameSceneData& frame, RG::TextureHandle src,
RG::TextureHandle dst)
: m_Frame(frame), m_Src(src), m_Dst(dst) {}
void Register(RG::RenderGraph& rg) override
{
rg.AddPass("Blit", [this](RG::RenderGraphContext& ctx) {
if (m_Frame.displayMode != kDisplayDirect) return; // Editor 模式空转
vk::ImageBlit region;
region.setSrcSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1})
.setDstSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1});
// src/dst 各按自己附件的物理尺寸取 offset(Direct 模式下
// 二者同为窗口尺寸;尺寸解耦时此写法也各自正确)
const vk::Extent2D srcExt = ctx.Extent(m_Src);
const vk::Extent2D dstExt = ctx.Extent(m_Dst);
const std::array<vk::Offset3D, 2> srcSpan = {
vk::Offset3D{0, 0, 0},
vk::Offset3D{static_cast<i32>(srcExt.width),
static_cast<i32>(srcExt.height), 1}};
const std::array<vk::Offset3D, 2> dstSpan = {
vk::Offset3D{0, 0, 0},
vk::Offset3D{static_cast<i32>(dstExt.width),
static_cast<i32>(dstExt.height), 1}};
region.setSrcOffsets(srcSpan).setDstOffsets(dstSpan);
ctx.cmd.blitImage(
ctx.Image(m_Src), vk::ImageLayout::eTransferSrcOptimal,
ctx.Image(m_Dst), vk::ImageLayout::eTransferDstOptimal,
{region}, vk::Filter::eLinear);
})
.Read(m_Src, RG::ResourceUse::TransferSrc)
.Write(m_Dst, RG::ResourceUse::TransferDst);
}
private:
FrameSceneData& m_Frame; // displayMode(帧契约)
RG::TextureHandle m_Src; // PresentTarget(tonemap 输出)
RG::TextureHandle m_Dst; // backbuffer
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_BLITPASS_H
@@ -0,0 +1,47 @@
//
// Created by atdunbg on 2026/9/23.
//
// BloomPass:Extract(HDR 软阈值)→ BlurH → BlurV,全部半分辨率。
// 一个"效果"对应多个 RG pass 的例子(Unity 的一个 ScriptableRenderPass
// 常含多个 PassEvent 阶段,同构)。
//
#ifndef HELLOVULKAN_BLOOMPASS_H
#define HELLOVULKAN_BLOOMPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <Engine/Renderer/SceneTypes.h>
#include <vulkan/vulkan.hpp>
#include <vector>
namespace Engine::Passes {
class BloomPass final : public RenderPass {
public:
BloomPass(FullscreenGpu& gpu, PostProcessParams& params,
FrameSceneData& frame,
RG::TextureHandle hdrColor,
RG::TextureHandle bloomA, RG::TextureHandle bloomB);
~BloomPass() override = default;
void Register(RG::RenderGraph& rg) override;
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
private:
FullscreenGpu& m_Gpu;
PostProcessParams& m_Params;
FrameSceneData& m_Frame; // bloomEnabled 开关(帧契约)
RG::TextureHandle m_HDR;
RG::TextureHandle m_A; // 提取 + 垂直模糊结果
RG::TextureHandle m_B; // 水平模糊中间结果
// per-slot 输入描述符
std::vector<vk::DescriptorSet> m_ExtractSets;
std::vector<vk::DescriptorSet> m_BlurHSets;
std::vector<vk::DescriptorSet> m_BlurVSets;
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_BLOOMPASS_H
@@ -0,0 +1,74 @@
//
// EditorGizmoPass:Editor gizmo 可视化叠加(billboard 图标 + 视锥线框)。
// editor 专属(HELLO_EDITOR):cpp TU 被 game 构建整体剔除
//(Engine/CMakeLists REMOVE_ITEM),本头文件只含声明、不引用任何
// 编辑器专属类型,game 构建可安全 include(零符号产出)。
//
// 与 PhysicsDebugPass 同构:
// - 写 HDRColor(Load 承接 Forward/PhysicsDebug 结果,WAW 链排其后,
// TAA 之前——gizmo 进 TAA/Bloom/Tonemap 主链路)
// - 深度只测不写(LessOrEqual,被场景几何正确遮挡),DontCare 不污染
// - Write(m_Depth) 声明深度 WAW 边:把本 pass 固定排在 HitProxy
// (深度附件 loadOp Clear)之前——gizmo 深度测试用的是场景深度
// - 无 PrepareFrame:Renderer 在上传窗口直接把 FrameSceneData 的
// gizmo 顶点灌进 ring VB(本 pass 只绑定绘制)
// - 空帧短路:两流顶点数全 0 直接 return,零 GPU 工作
//
// 两流两状态(Shader Object 动态状态跨 pass 残留教训:公共状态无条件
// 直写,差异状态各流内直写):
// - 线流(GizmoWireVertex):LineList + lineWidth(1.0) + 无混合直写,
// 复用 debug_line.vert/frag(世界空间线段 + push viewProj 同契约)
// - 图标流(GizmoQuadVertex):TriangleList + 预乘 alpha 混合
// (ONE, ONE_MINUS_SRC_ALPHA),gizmo_billboard.vert/frag SDF 造型
//
#ifndef HELLOVULKAN_EDITORGIZMOPASS_H
#define HELLOVULKAN_EDITORGIZMOPASS_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <vulkan/vulkan.hpp>
namespace Engine {
class Device;
class DescriptorManager;
class UniformBuffer;
namespace Passes {
class EditorGizmoPass final : public RenderPass {
public:
EditorGizmoPass(Device& device, ShaderManager& shaders,
DescriptorManager& descriptors,
FrameSceneData& frame,
RG::TextureHandle hdrColor,
RG::TextureHandle depth,
UniformBuffer& iconVB, UniformBuffer& wireVB);
~EditorGizmoPass();
EditorGizmoPass(const EditorGizmoPass&) = delete;
EditorGizmoPass& operator=(const EditorGizmoPass&) = delete;
void Register(RG::RenderGraph& rg) override;
private:
Device& m_Device;
DescriptorManager& m_Descriptors;
FrameSceneData& m_Frame;
RG::TextureHandle m_HDRColor;
RG::TextureHandle m_Depth;
UniformBuffer& m_IconVB; // per-slot ring(Renderer 持有生命周期)
UniformBuffer& m_WireVB;
vk::PipelineLayout m_Layout; // 无描述符,push = viewProj 64B
ShaderManager::ShaderRef m_WireVert, m_WireFrag; // debug_line 复用
ShaderManager::ShaderRef m_IconVert, m_IconFrag; // gizmo_billboard
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_EDITORGIZMOPASS_H
@@ -0,0 +1,60 @@
//
// Created by atdunbg on 2026/9/23.
//
// ForwardPass:相机视角渲染到 HDR 颜色图(线性光照值)。
// 显示变换不在本 pass——HDRColor 经 Tonemap 归一后才写 Backbuffer
// (对齐 UE/Unity:SceneColor(HDR) → tonemapper → UI)。
//
#ifndef HELLOVULKAN_FORWARDPASS_H
#define HELLOVULKAN_FORWARDPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Renderer/SceneTypes.h>
namespace Engine::Passes {
class ForwardPass final : public RenderPass {
public:
ForwardPass(SceneGpu& gpu, FrameSceneData& frame,
RG::TextureHandle hdrColor, RG::TextureHandle velocity,
RG::TextureHandle depth,
RG::TextureHandle sceneColor,
const std::array<RG::TextureHandle, kMaxCascades>& shadowMaps)
: m_Gpu(gpu), m_Frame(frame),
m_HDRColor(hdrColor), m_Velocity(velocity),
m_Depth(depth), m_SceneColor(sceneColor),
m_ShadowMaps(shadowMaps) {}
void Register(RG::RenderGraph& rg) override;
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
private:
// 两段渲染共享的命令录制(shader/描述符绑定 + 几何/光栅/视口状态)。
// translucent=true 绑定透射变体(折射采样场景色副本);
// 深度写与混合状态由各渲染实例作为初始态设置,见 ForwardPass.cpp
void RecordSharedState(const RG::RenderGraphContext& ctx,
vk::CommandBuffer cmd, bool translucent) const;
// 透明队列绘制单位总数(= 各批实例数之和;排序后扁平单位序)
u32 TransparentUnitCount() const;
// 绘制透明单位区间 [start,end)(批次内连续实例合并为一次 draw):
// 透明渲染实例 + 预乘 alpha 混合 + 只测不写深度的全部状态
void DrawTransparentRange(const RG::RenderGraphContext& ctx,
vk::CommandBuffer cmd,
u32 start, u32 end) const;
SceneGpu& m_Gpu;
FrameSceneData& m_Frame;
RG::TextureHandle m_HDRColor;
RG::TextureHandle m_Velocity; // TAA 运动向量(第二颜色附件)
RG::TextureHandle m_Depth;
RG::TextureHandle m_SceneColor; // 场景色副本(透射折射采样源)
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps;
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_FORWARDPASS_H
@@ -0,0 +1,106 @@
//
// Created by atdunbg on 2026/9/23.
//
// FullscreenGpu:全屏后处理共享装配(Tonemap/Bloom 系 pass 共用)。
//
// 全屏 pass 的共性收口在这里:Set0 = {输入0, 输入1}、push 16B 通用结构
// (规避 VUID-08878 的"range 定义必须一致")、无顶点输入全屏三角、
// 公共录制函数。新增全屏效果 = 复用本类 + 新增一个 frag shader。
//
#ifndef HELLOVULKAN_FULLSCREENGPU_H
#define HELLOVULKAN_FULLSCREENGPU_H
#include <Engine/Core/Types.h>
#include <Engine/RenderGraph/RenderGraph.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <vulkan/vulkan.hpp>
namespace Engine {
class Device;
class DescriptorManager;
namespace Passes {
// 全屏 pass 通用 push(16B):所有全屏 pass 统一声明与推送
struct FullscreenPush {
f32 exposure;
f32 param0; // 各 pass 自定语义(tonemap: aces 开关)
f32 param1;
f32 param2;
};
// 后处理数值参数(ImGui 可调;Renderer 持有,pass 按引用读取)。
// 运行时开关/调试视图不在这里——统一进 FrameSceneData(帧契约)
struct PostProcessParams {
f32 exposure = 0.6f;
bool useACES = true;
// HDR 域亮度阈值。注意不能设 1.0:光滑金属反射亮天空的 HDR 值
// 普遍 2~50,阈值为 1 会让整片金属表面进 bloom → 白色泛光糊满
//(怀表铜壳"纯白"元凶)。3.0 ≈ ACES 后接近白的真高光才泛光
f32 bloomThreshold = 3.0f;
f32 bloomStrength = 0.8f; // 叠加强度
};
// 调试视图枚举:TonemapPass 按值把对应中间目标路由上屏。
// 与 Renderer 的 DebugViewNames 表下标一一对应
enum DebugView : int {
kDebugFinal = 0, // 正常画面(tonemap 输出)
kDebugTemporalDiff, // 帧历史差异(TAA 链路验证)
kDebugHDR, // Forward 原始 HDR(无 bloom/TAA)
kDebugTAAColor, // TAA resolve 后
kDebugBloom, // bloom 模糊结果
kDebugVelocity, // 运动向量(|uv| 增益显示)
kDebugShadowMap0, // CSM 级联 0 深度(近=白)
kDebugShadowMap1, // 级联 1
kDebugShadowMap2, // 级联 2
kDebugEnvCube, // IBL 环境 cube(经纬展开)
kDebugIrradiance, // IBL 漫反射辐照度(32² 低频)
kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级)
kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道)
kDebugTransparent, // 仅透明队列(Skybox/Forward 跳过绘制,看透明段内容)
kDebugViewCount
};
class FullscreenGpu {
public:
FullscreenGpu(Device& device, DescriptorManager& descriptors,
ShaderManager& shaders);
~FullscreenGpu(); // 只清 sampler/layout(shader 随引用自动销毁)
FullscreenGpu(const FullscreenGpu&) = delete;
FullscreenGpu& operator=(const FullscreenGpu&) = delete;
vk::DescriptorSetLayout setLayout; // Set0 = {输入0, 输入1, 调试输入}
vk::PipelineLayout layout; // push 16B(FullscreenPush)
vk::Sampler sampler;
// shader 引用计数句柄(析构自动销毁,零手工清理)
ShaderManager::ShaderRef vert, extractFrag, blurFrag, tonemapFrag;
ShaderManager::ShaderRef diffFrag; // temporal_diff(帧历史调试)
ShaderManager::ShaderRef taaFrag; // taa(时域抗锯齿 resolve)
// 各 pass 自己持有 per-slot set;分配统一走这里
vk::DescriptorSet AllocateSet();
// 输入图片绑定(RG 重编译后重绑;物理镜像按槽位各一份)
void UpdateSetInput(vk::DescriptorSet set, u32 binding,
RG::RenderGraph& rg, RG::TextureHandle input,
u32 slot);
// 全屏 pass 公共录制:动态状态/draw 全一致,仅附件 view、
// 输入 set、push 值不同
void RecordFullscreen(RG::RenderGraphContext& ctx, vk::ImageView view,
const ShaderManager::ShaderRef& frag,
const vk::DescriptorSet& set,
const FullscreenPush& push);
private:
Device& m_Device;
DescriptorManager& m_Descriptors;
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_FULLSCREENGPU_H
@@ -0,0 +1,92 @@
//
// HitProxyPass:UE 式 GPU 拾取(像素级 id pass + 点击 readback)。
//
// 两个 RG pass:
// "HitProxy" —— 拾取请求帧把不透明 + 透明 batch 用 id 管线画进
// R32Uint 目标(片元值 = 实体位模式)。深度自测自写
// (重清主深度):遮挡语义 = 可见表面胜出;运行在
// Forward/ForwardTransL*/PhysicsDebug 之后的 WAW 链尾,
// 主链下游无人再读深度,下帧 Forward 照常重新 Clear
// "HitProxyCopy" —— 请求像素 1×1 CopyImageToBuffer 进 host-visible
// 暂存。TransferSrc use 让 RG 自动完成
// eColorAttachmentOptimal → eTransferSrcOptimal 迁移
//
// 无请求帧零绘制零拷贝(barrier 照常插入,GPU 无实际工作)。
// 结果流:Renderer::TakePickResult(waitIdle 保证提交完成)→ 映射暂存。
//
#ifndef HELLOVULKAN_HITPROXYPASS_H
#define HELLOVULKAN_HITPROXYPASS_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/Vulkan/Buffer.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <vulkan/vulkan.hpp>
namespace Engine {
class Device;
class ShaderManager;
class DescriptorManager;
class UniformBuffer;
namespace Passes {
class SceneGpu;
class HitProxyPass final : public RenderPass {
public:
HitProxyPass(Device& device, MemoryAllocator& allocator,
ShaderManager& shaders, DescriptorManager& descriptors,
SceneGpu& gpu, FrameSceneData& frame,
RG::TextureHandle idTarget, RG::TextureHandle depth);
~HitProxyPass();
HitProxyPass(const HitProxyPass&) = delete;
HitProxyPass& operator=(const HitProxyPass&) = delete;
void Register(RG::RenderGraph& rg) override;
// 有待取结果(拷贝已录制进当帧命令缓冲)
bool HasPendingResult() const { return m_ResultValid; }
// 取走拷贝结果(一次性)。调用方保证 GPU 已 idle
//(Renderer::TakePickResult 内 waitIdle)
bool TakeResult(u32& outBits);
#ifdef HELLO_EDITOR
// Editor gizmo 拾取代理壳接入(editor 专属;Renderer
// BuildGraph 内 Attach——共享 gizmo 代理 ring VB,本 pass
// 用 gizmo_pick 管线重画进 R32Uint,与 mesh 同一深度解算)
void AttachGizmoProxy(UniformBuffer& vb) { m_GizmoProxyVB = &vb; }
#endif
private:
Device& m_Device;
SceneGpu& m_Gpu;
FrameSceneData& m_Frame;
RG::TextureHandle m_IdTarget;
RG::TextureHandle m_Depth;
vk::PipelineLayout m_Layout; // Set0 = uboSetLayout,无 push
ShaderManager::ShaderRef m_Vert, m_Frag; // 析构自动销毁
// 1×1 像素暂存(host visible,持久映射);拷贝录制时置位
Scope<Buffer> m_Staging;
bool m_ResultValid = false;
#ifdef HELLO_EDITOR
// gizmo 代理壳拾取(editor 专属,game 构建零成员):独立
// layout(无描述符,push = viewProj)+ gizmo_pick shader 对 +
// 代理 ring VB 非拥有指针(Renderer 持有生命周期)
vk::PipelineLayout m_GizmoLayout;
ShaderManager::ShaderRef m_GizmoVert, m_GizmoFrag;
UniformBuffer* m_GizmoProxyVB = nullptr;
#endif
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_HITPROXYPASS_H
@@ -0,0 +1,51 @@
//
// Created by atdunbg on 2026/9/23.
//
// ImGuiPass:叠加层(loadOp=Load,不清屏)。
// 框架只负责把 draw data 录进 cmd;UI 内容全部由派生类在
// OnRenderOverlay() 里构建,框架自身不画任何 ImGui 控件。
// 未启用叠加层时空转(begin/end 一次空 rendering region)。
//
#ifndef HELLOVULKAN_IMGUIPASS_H
#define HELLOVULKAN_IMGUIPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h> // FrameSceneData(displayMode 帧契约)
namespace Engine {
class ImGuiLayer;
namespace Passes {
class ImGuiPass final : public RenderPass {
public:
// 二级指针:Renderer::SetImGuiLayer 在 Register 之后才可能被调
//(EnableImGui 时机由派生类决定),执行期解引用取最新值。
// presentTarget:ImGui::Image 采样的场景纹理(tonemap 输出)。
// loadOp 按 displayMode 分支:Direct = Load(blit 已铺场景),
// Viewport = Clear(唯一写入者,清"桌面底色")
// previewAtlas:材质球预览 SDR atlas(MaterialPreviewPass 写、
// ImGui::Image 按槽位采样;声明读 = eSampled usage 推导 +
// ColorAttachment→ShaderRead barrier)
ImGuiPass(ImGuiLayer** layer, FrameSceneData& frame,
RG::TextureHandle backbuffer, RG::TextureHandle presentTarget,
RG::TextureHandle previewAtlas)
: m_Layer(layer), m_Frame(frame), m_Backbuffer(backbuffer),
m_PresentTarget(presentTarget), m_PreviewAtlas(previewAtlas) {}
void Register(RG::RenderGraph& rg) override;
private:
ImGuiLayer** m_Layer;
FrameSceneData& m_Frame; // displayMode(帧契约)
RG::TextureHandle m_Backbuffer;
RG::TextureHandle m_PresentTarget;
RG::TextureHandle m_PreviewAtlas;
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_IMGUIPASS_H
@@ -0,0 +1,346 @@
//
// Created by atdunbg on 2026/10/5.
//
// MaterialPreviewPass:材质球预览缩略图(editor 专属,HELLO_EDITOR 裁剪)。
//
// 架构对齐 UE FAssetThumbnailPool / Unity AssetPreview:
// - 渲染一次缓存:tile 内容跨帧持久(HDR atlas loadOp=Load),只有
// "脏 tile"(新材质 / 参数版本变化 / 镜像失效)消耗每帧渲染预算。
// RG 物理镜像按帧槽位各一份(in-flight 三缓冲),tile 完成标准 =
// 三个槽位的镜像都画过(paintedSlots 位掩码)——否则 ImGui 按当前
// 槽位采样会 1/3 帧有图 2/3 帧是垃圾(按帧率闪烁)。完成后干净帧
// 两 pass 零 GPU 工作
// - 只请求可见项:UI 对滚进视口的行发幂等 keepalive 请求,池按
// Material 身份(serial + editVersion)判缓存命中/标脏
// - 失效模型:Material::UpdateParams bump editVersion → 下帧重渲该
// tile(拖滑块期间单 tile 实时,其余零 GPU 工作);材质销毁/换绑
// 靠 lastUseFrame 老化 LRU 驱逐,池内不跨帧持有 Material 指针
//
// 渲染本体(与场景零复制的 WYSIWYG 契约,原样保留):
// - 几何 = 程序化 UV 球(世界空间原点,无模型矩阵——预览 set0 的
// ObjectBuffer 恒 identity,与 simple_shader.vert 的 GPUDriven
// 契约兼容:gl_InstanceIndex=0 → models[0]=I)
// - 光照 = 与场景完全相同的 simple_shader.vert/frag(同一 SPIR-V、
// 同一描述符布局):set1 直接绑目标材质的描述符集
// - set2 绑 SceneGpu::previewEnvSets(IBL 三件套锁定中性环境——UE
// ThumbnailRenderer 语义:观察条件恒定,金属球不随场景环境变黑;
// 级联/场景色副本与场景同步)
// - 固定棚拍布光(自建 set0 UBO,内容恒定只写一次)
//
// 两个固定尺寸 atlas(不随渲染分辨率缩放):
// PreviewHDR(R16G16B16A16Sfloat)← 本 pass 按脏 tile 清矩形 + 绘制
// PreviewSDR(R8G8B8A8Unorm) ← tonemap(仅有渲染的帧执行),
// ImGuiPass 声明读,ImGui 按槽位采样
//
// 本 pass 消费 Renderer 每帧换入的脏 tile 表(PreviewTilePool::TakeRenders
// 产出,预算 kPreviewRendersPerFrame);表空 = 两 pass 零 GPU 工作。
//
#ifndef HELLOVULKAN_MATERIALPREVIEWPASS_H
#define HELLOVULKAN_MATERIALPREVIEWPASS_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Material.h> // 完整类型:池内联函数读 serial/editVersion
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <vulkan/vulkan.hpp>
#include <algorithm>
#include <array>
#include <vector>
namespace Engine {
class Device;
class MemoryAllocator;
class DescriptorManager;
class UniformBuffer;
class Mesh;
class Material;
class Cubemap;
class Buffer;
namespace Passes {
class SceneGpu;
class FullscreenGpu;
// atlas 布局:5×5 tile × 256²。tile 总数 = 并发池容量(不是槽位
// 上限):UI 只对可见行请求,池按 LRU 换页支撑任意材质数
inline constexpr u32 kPreviewAtlasSize = 1280;
inline constexpr u32 kPreviewTileSize = 256;
inline constexpr u32 kPreviewTilesPerSide = 5;
inline constexpr u32 kPreviewTileCount =
kPreviewTilesPerSide * kPreviewTilesPerSide;
// 单 tile 回读字节数(SDR atlas 为 RGBA8)
inline constexpr u32 kPreviewTileBytes =
kPreviewTileSize * kPreviewTileSize * 4;
// 本帧待渲染的脏 tile(Material* 来自本帧 UI 请求,活指针)
struct PreviewDirtyTile {
Material* material = nullptr;
u32 tile = 0;
};
// 本帧待回读烘焙的 tile(key = 调用方令牌,Renderer 不解释)。
// 只收录"本帧确实被渲染"的 tile,故条数 ≤ 渲染预算
struct PreviewBakeTile {
u64 key = 0;
u32 tile = 0;
};
// 编辑器 tile 池(Renderer 持有,UE FAssetThumbnailPool 语义)。
// 缓存键 = Material serial(出生唯一号,防指针地址复用的 ABA)
// + editVersion(参数代数)。游戏构建整体编译裁剪,无运行时成本
class PreviewTilePool {
public:
// 每帧渲染预算:脏 tile 多时逐帧消化(浏览器缩略图烘焙 / 滚动
// 列表渐进出图)。预览球是 128 三角形的 256² 直绘,8 个/帧的
// GPU 成本可忽略;烘焙吞吐 = 预算(回读只收当帧渲染过的 tile)
static constexpr u32 kPreviewRendersPerFrame = 8;
// UI 构建段逐可见行调用(幂等 keepalive,刷新 LRU + 脏检测)。
// 返回 tile:>=0 = 三槽位镜像内容齐备可显示(含重渲期间的旧
// 内容——拖滑块先显示旧球,三帧内全部槽位收敛到新球,无闪烁);
// -1 = 池满不可驱逐 / 尚未画满三槽位(UI 显示占位)
i32 Request(Material* material)
{
if (!material) return -1;
const u64 serial = material->GetSerial();
Entry* slot = nullptr;
for (auto& e : m_Entries) {
if (e.serial == serial) { slot = &e; break; }
}
if (!slot) {
// 分配:空槽优先;满则驱逐最久未用(仅限本帧未使用
// 者——可见行并发数 < 容量时不发生)
u64 oldest = ~0ull;
for (auto& e : m_Entries) {
if (e.serial == 0) { slot = &e; break; }
if (e.lastUseFrame < m_Frame &&
e.lastUseFrame < oldest) {
oldest = e.lastUseFrame;
slot = &e;
}
}
if (!slot) return -1;
slot->serial = serial;
slot->paintedSlots = 0;
slot->trackedVersion = 0;
}
const u32 tileIdx =
static_cast<u32>(slot - m_Entries.data());
slot->lastUseFrame = m_Frame;
const u32 ver = material->GetEditVersion();
const bool displayable =
slot->paintedSlots == kAllSlotsMask;
if (!displayable || slot->trackedVersion != ver) {
// 标脏(同 tile 本帧去重——同一材质可被多行共享)。
// 未画满三槽位的 tile 每帧重入队,直到逐槽位补齐;
// 重入队刷新活指针(跳帧滞留也不会引用已销毁材质)
bool queued = false;
for (auto& d : m_FrameDirty) {
if (d.tile == tileIdx) {
d.material = material;
queued = true;
break;
}
}
if (!queued) {
m_FrameDirty.push_back({material, tileIdx});
}
}
return displayable ? static_cast<i32>(tileIdx) : -1;
}
// 烘焙请求(浏览器缩略图回读):强制标脏——即使 tile 已
// displayable 也重渲一次,保证"当帧渲染 → 当帧拷贝回读"闭环
// (回读只收当帧渲染过的 tile)。返回槽位 tile 索引;-1 =
// 池满无槽(调用方下帧重试)。不参与 paintedSlots 记账
i32 RequestBake(Material* material)
{
if (!material) return -1;
const u64 serial = material->GetSerial();
Entry* slot = nullptr;
for (auto& e : m_Entries) {
if (e.serial == serial) { slot = &e; break; }
}
if (!slot) {
u64 oldest = ~0ull;
for (auto& e : m_Entries) {
if (e.serial == 0) { slot = &e; break; }
if (e.lastUseFrame < m_Frame &&
e.lastUseFrame < oldest) {
oldest = e.lastUseFrame;
slot = &e;
}
}
if (!slot) return -1;
slot->serial = serial;
slot->paintedSlots = 0;
slot->trackedVersion = 0;
}
slot->lastUseFrame = m_Frame;
const u32 tileIdx =
static_cast<u32>(slot - m_Entries.data());
bool queued = false;
for (auto& d : m_FrameDirty) {
if (d.tile == tileIdx) { d.material = material; queued = true; break; }
}
if (!queued) m_FrameDirty.push_back({material, tileIdx});
return static_cast<i32>(tileIdx);
}
// Render 段取本帧预算内的脏 tile(frameSlot = 本帧渲染槽位,
// 该槽位镜像在本帧被写入)。渲染语义(清矩形+画球)由 pass
// 执行后该槽位内容即有效。剩余脏 tile 保持未取——下帧可见行
// keepalive 请求会以新活指针重新入队,池绝不跨帧持有指针
const std::vector<PreviewDirtyTile>& TakeRenders(u32 frameSlot)
{
++m_Frame;
m_Active.clear();
const u32 budget = std::min<u32>(
kPreviewRendersPerFrame,
static_cast<u32>(m_FrameDirty.size()));
for (u32 i = 0; i < budget; ++i) {
const auto& d = m_FrameDirty[i];
Entry& e = m_Entries[d.tile];
e.trackedVersion = d.material->GetEditVersion();
e.paintedSlots |= (1u << frameSlot);
m_Active.push_back(d);
}
m_FrameDirty.clear();
return m_Active;
}
// 材质实例即将销毁/重建(ReloadMaterialAsset):清脏表引用
// + 按 serial 清槽位。同 UI 段"Request → Reload"顺序下,脏
// 表里的裸指针当帧 Render 段会被解引用——必须先 drop
void DropMaterial(const Material* material)
{
if (!material) return;
const u64 serial = material->GetSerial();
std::erase_if(m_FrameDirty,
[&](const PreviewDirtyTile& d)
{ return d.material == material; });
for (auto& e : m_Entries) {
if (e.serial == serial) {
e.serial = 0;
e.paintedSlots = 0;
e.trackedVersion = 0;
}
}
}
// RG 重编译后物理镜像全部换新(三槽位内容清零)——池记录的
// paintedSlots 全部失效。可见行 keepalive 请求自然重入队补画
void InvalidateAll()
{
for (auto& e : m_Entries) {
e.paintedSlots = 0;
e.trackedVersion = 0;
}
}
private:
struct Entry {
u64 serial = 0; // 0 = 空槽(Material serial 从 1 起)
u32 trackedVersion = 0; // atlas 内容对应的参数版本
u64 lastUseFrame = 0; // LRU / 老化驱逐判据
u32 paintedSlots = 0; // 已画过的帧槽位镜像位掩码
};
// 全槽位掩码:三份 in-flight 镜像都画过 = 内容可显示
static constexpr u32 kAllSlotsMask =
(1u << kMaxFramesInFlight) - 1;
std::array<Entry, kPreviewTileCount> m_Entries;
std::vector<PreviewDirtyTile> m_FrameDirty; // 本帧标脏(UI 段)
std::vector<PreviewDirtyTile> m_Active; // 预算内(Render 段)
u64 m_Frame = 0;
};
class MaterialPreviewPass final : public RenderPass {
public:
// dirtyTiles = Renderer 持有的当帧脏表(Render 段换入,只读)
// bakeTiles = Renderer 持有的当帧回读表(只读;条数 ≤ 渲染预算)
MaterialPreviewPass(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
SceneGpu& gpu, FullscreenGpu& fullscreen,
RG::TextureHandle hdrAtlas,
RG::TextureHandle velocityAtlas,
RG::TextureHandle sdrAtlas,
const std::vector<PreviewDirtyTile>&
dirtyTiles,
const std::vector<PreviewBakeTile>&
bakeTiles);
~MaterialPreviewPass() override;
// 本槽位的回读暂存(host-visible,持久映射):Renderer 在该
// 槽位 fence 通过后 memcpy 取走烘焙像素
Buffer* BakeStaging(u32 frameSlot) const
{
return frameSlot < kMaxFramesInFlight
? m_BakeStaging[frameSlot].get()
: nullptr;
}
void Register(RG::RenderGraph& rg) override;
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
// 调试视图 cube 槽(binding 8-11)绑真环境资源——cube 槽类型
// 必须 Cube,2D atlas 占位会触发 VUID-07752
void OnEnvironmentChanged(const Cubemap* env) override;
private:
// 单个材质球:tile 视口/裁剪 + 材质 set1 + 混合态 + 球绘制
void RecordBall(vk::CommandBuffer cmd, u32 tile,
const Material& material) const;
Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_Descriptors;
SceneGpu& m_Gpu;
FullscreenGpu& m_Fullscreen;
RG::TextureHandle m_HDRAtlas;
RG::TextureHandle m_VelocityAtlas;
RG::TextureHandle m_SDRAtlas;
const std::vector<PreviewDirtyTile>& m_DirtyTiles;
const std::vector<PreviewBakeTile>& m_BakeTiles;
// 回读暂存:每帧槽位一份(容量 = 渲染预算 × 单 tile 字节)。
// 拷贝录制在该槽位命令缓冲,Renderer 在该槽位 fence 通过后读
std::array<Scope<Buffer>, kMaxFramesInFlight> m_BakeStaging;
// ---- 预览专用 set0(与 uboSetLayout 同布局,内容恒定)----
// UBO = 固定棚拍相机/布光;SSBO = GPUDriven 契约的最小占位
//(1 实例 identity 矩阵 + skin slot0 identity + 实体 id 占位,
// vert 按 gl_InstanceIndex=0 读取)
Scope<UniformBuffer> m_PreviewUbo;
Scope<UniformBuffer> m_ObjectBuffer;
Scope<UniformBuffer> m_PrevObjectBuffer;
Scope<UniformBuffer> m_SkinOffsetBuffer;
Scope<UniformBuffer> m_SkinPaletteBuffer;
Scope<UniformBuffer> m_EntityIdBuffer;
vk::DescriptorSet m_Set0;
// 程序化 UV 球(世界空间原点半径 1;材质非拥有 = nullptr,
// 材质在绘制时经 set1 绑定,不进 Mesh)
Scope<Mesh> m_Sphere;
// tonemap per-slot set(FullscreenGpu 布局:binding0 = HDR atlas,
// binding1 同源占位——tonemap.frag 终值路径恒采样 bloom 图,
// 强度 0 丢弃)
std::array<vk::DescriptorSet, kMaxFramesInFlight> m_TonemapSets;
// 当前环境(cube 调试槽源;注册时与 SetEnvironment 时由
// Renderer 通知)
const Cubemap* m_Env = nullptr;
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_MATERIALPREVIEWPASS_H
@@ -0,0 +1,52 @@
//
// OutlinePass:选中描边后处理(UE 编辑器选中轮廓语义)。
//
// 对 SelectionMask 做边缘检测,alpha 混合叠加在 Tonemap 之后的
// PresentTarget 上——描边是恒定 LDR UI 色,不进 bloom/曝光/TAA。
// 无选中帧跳过绘制(SideEffect 保活,见 HitProxy 教训)。
//
#ifndef HELLOVULKAN_OUTLINEPASS_H
#define HELLOVULKAN_OUTLINEPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <vulkan/vulkan.hpp>
namespace Engine {
namespace Passes {
class FullscreenGpu;
class OutlinePass final : public RenderPass {
public:
// color:描边色(LDR);widthPx:描边宽度(物理像素)
OutlinePass(FullscreenGpu& gpu, ShaderManager& shaders,
FrameSceneData& frame,
RG::TextureHandle mask, RG::TextureHandle present,
const glm::vec3& color = glm::vec3(1.0f, 0.62f, 0.05f),
f32 widthPx = 6.0f);
OutlinePass(const OutlinePass&) = delete;
OutlinePass& operator=(const OutlinePass&) = delete;
void Register(RG::RenderGraph& rg) override;
void OnTargetsRebuilt(RG::RenderGraph& rg) override;
private:
FullscreenGpu& m_Gpu;
FrameSceneData& m_Frame;
RG::TextureHandle m_Mask;
RG::TextureHandle m_Present;
glm::vec3 m_Color;
f32 m_WidthPx;
ShaderManager::ShaderRef m_Frag; // 析构自动销毁
std::vector<vk::DescriptorSet> m_Sets; // per-frame slot
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_OUTLINEPASS_H
@@ -0,0 +1,95 @@
//
// PhysicsDebugPass:调试图元叠加 pass(双源)。
// 物理源 = PhysicsWorld 收集的 PhysicsDebugFrame(collider 线框/
// 约束,Application 注入帧缓冲指针)
// 通用源 = Engine::Debug 调试服务(脚本/任意层的 Line/Arrow/Wire*,
// Unity Debug.DrawLine 同位;批次自带线宽/深度测试/拓扑)
//
// 画在 HDRColor(Forward 之后、TAA 之前):调试图元进入场景主链路——
// TAA 时域累积抗锯齿、Bloom/Tonemap 正常消费,kDebugTransparent 诊断
// 模式下也可见。深度只测不写(被不透明几何正确遮挡),无混合直写
// (fullscreen pass 状态泄漏教训)。
//
// 空帧零绘制:两源顶点数均 0 时提前短路。
//
#ifndef HELLOVULKAN_PHYSICSDEBUGPASS_H
#define HELLOVULKAN_PHYSICSDEBUGPASS_H
#include <Engine/Core/Types.h>
#include <Engine/Debug/DebugDraw.h>
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/Vulkan/UniformBuffer.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <vector>
#include <vulkan/vulkan.hpp>
namespace Engine {
struct PhysicsDebugFrame; // PhysicsDebugTypes.h(Jolt 零依赖契约)
class Device;
class ShaderManager;
class DescriptorManager;
namespace Passes {
class PhysicsDebugPass final : public RenderPass {
public:
// 顶点预算(每帧槽位,按顶点数):物理调试几十个 body 的
// 线框 + 箭头远用不满;溢出截断并告警一次
static constexpr u32 kMaxLineVerts = 1 << 16;
static constexpr u32 kMaxTriVerts = 1 << 16;
PhysicsDebugPass(Device& device, MemoryAllocator& allocator,
ShaderManager& shaders,
DescriptorManager& descriptors,
FrameSceneData& frame,
RG::TextureHandle hdrColor,
RG::TextureHandle depth,
UniformBuffer& lineVB, UniformBuffer& triVB,
UniformBuffer& debugVB);
~PhysicsDebugPass();
PhysicsDebugPass(const PhysicsDebugPass&) = delete;
PhysicsDebugPass& operator=(const PhysicsDebugPass&) = delete;
void Register(RG::RenderGraph& rg) override;
// 每帧 Renderer::Render 里(该槽位 fence 等待后)上传当帧
// 图元。dbg 为 nullptr = 通用调试源禁用/空
void PrepareFrame(u32 frameSlot, const PhysicsDebugFrame& src,
const Debug::DebugFrame* dbg);
private:
// 一个可绘制段:独立拓扑/线宽/深度测试(动态状态逐批设置)
struct Span {
bool tris;
f32 width;
bool depthTest;
u32 first; // 顶点偏移(对应 VB 内顶点序号)
u32 count;
};
Device& m_Device;
DescriptorManager& m_Descriptors;
FrameSceneData& m_Frame;
RG::TextureHandle m_HDRColor;
RG::TextureHandle m_Depth;
UniformBuffer& m_LineVB; // per-slot ring(物理源,Renderer 持有)
UniformBuffer& m_TriVB;
UniformBuffer& m_DebugVB; // 通用调试源 ring(Renderer 持有)
vk::PipelineLayout m_Layout; // 无描述符,push = viewProj 64B
ShaderManager::ShaderRef m_Vert, m_Frag; // 析构自动销毁
std::array<u32, kMaxFramesInFlight> m_LineCounts{};
std::array<u32, kMaxFramesInFlight> m_TriCounts{};
std::array<std::vector<Span>, kMaxFramesInFlight> m_Spans;
bool m_TruncationWarned = false;
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_PHYSICSDEBUGPASS_H
@@ -0,0 +1,32 @@
//
// Created by atdunbg on 2026/9/23.
//
// PresentPass:transition 到 ePresentSrcKHR。
// layout 转换由 RG 的 barrier 完成,实际 vkQueuePresentKHR 由 Renderer 驱动。
//
#ifndef HELLOVULKAN_PRESENTPASS_H
#define HELLOVULKAN_PRESENTPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
namespace Engine::Passes {
class PresentPass final : public RenderPass {
public:
explicit PresentPass(RG::TextureHandle backbuffer)
: m_Backbuffer(backbuffer) {}
void Register(RG::RenderGraph& rg) override
{
rg.AddPass("Present", [](RG::RenderGraphContext&) {})
.Read(m_Backbuffer, RG::ResourceUse::Present);
}
private:
RG::TextureHandle m_Backbuffer;
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_PRESENTPASS_H
@@ -0,0 +1,42 @@
//
// Created by atdunbg on 2026/9/23.
//
// RenderPass:渲染 pass 对象基类(Unity ScriptableRenderPass 思路)。
//
// pass 自己负责两件事,框架(Renderer)不再手写"超级函数":
// Register —— 向 RenderGraph 声明资源使用 + 录制 lambda;
// 执行序由 RG 拓扑排序决定(writer 必先于 reader),
// 与注册顺序无关
// OnTargetsRebuilt —— RG 重编译(resize / vsync 切换)后重绑描述符
// (物理镜像按槽位可能全部换新)
//
// pass 之间不互相依赖:数据流只经 RG 资源句柄。新增效果 = 新增一个
// RenderPass 子类 + Renderer::BuildGraph 里注册一行,其它代码不动。
//
#ifndef HELLOVULKAN_RENDERPASS_H
#define HELLOVULKAN_RENDERPASS_H
#include <Engine/RenderGraph/RenderGraph.h>
namespace Engine {
class Cubemap;
}
namespace Engine::Passes {
class RenderPass {
public:
virtual ~RenderPass() = default;
virtual void Register(RG::RenderGraph& rg) = 0;
virtual void OnTargetsRebuilt(RG::RenderGraph& rg) { (void)rg; }
// 环境资产换绑(动态切换):引用环境描述符的 pass 重绑自己的 set。
// 默认无环境影响;env 恒非空(Renderer 有中性兜底)
virtual void OnEnvironmentChanged(const Cubemap* /*env*/) {}
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_RENDERPASS_H
@@ -0,0 +1,123 @@
//
// Created by atdunbg on 2026/9/23.
//
// SceneGpu:场景级 GPU 共享装配(Shadow/Forward 及未来几何类 pass 共用)。
//
// 从 Renderer 拆出的装配体:set 布局 / pipeline 布局 / shader object /
// 采样器 / per-slot 描述符集合。新增几何 pass(延迟、调试线框等)直接
// 复用本装配,不必碰 Renderer。
//
#ifndef HELLOVULKAN_SCENEGPU_H
#define HELLOVULKAN_SCENEGPU_H
#include <Engine/Core/FrameContext.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/RenderGraph/RenderGraph.h>
#include <Engine/Vulkan/ShaderManager.h>
#include <vulkan/vulkan.hpp>
#include <array>
#include <vector>
namespace Engine {
class Device;
class DescriptorManager;
class UniformBuffer;
class Cubemap;
class Buffer;
class MemoryAllocator;
namespace Passes {
class SceneGpu {
public:
SceneGpu(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
ShaderManager& shaders,
UniformBuffer& ubo, UniformBuffer& objectBuffer,
UniformBuffer& prevObjectBuffer,
UniformBuffer& skinOffsetBuffer,
UniformBuffer& skinPaletteBuffer,
UniformBuffer& entityIdBuffer);
~SceneGpu(); // 只清 sampler/layout/set(shader 随引用自动销毁)
SceneGpu(const SceneGpu&) = delete;
SceneGpu& operator=(const SceneGpu&) = delete;
// ---- MDI 间接绘制 ----
// CPU 打包的命令/count 落进本槽位 ring(Renderer 在 fence
// 等待后调用——与 UBO/SSBO 上传同一安全窗口)
void UploadIndirect(u32 frameSlot, const FrameSceneData& frame);
// 一次组绘制:bind 共享 megabuffer + drawIndexedIndirectCount
void DrawIndirectGroup(vk::CommandBuffer cmd, u32 frameSlot,
u32 viewIndex, const IndirectGroup& g);
// ---- set 布局(shader 契约,改绑定必须同步 shaders/)----
vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer,
// SkinOffsets, SkinPalettes, EntityIds}
vk::DescriptorSetLayout textureSetLayout; // Set1 = {材质 4 槽 + 参数 UBO}
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube,
// prefiltered cube, BRDF LUT, 场景色副本}
vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO)
vk::PipelineLayout shadowLayout; // Set0,push = lightVP 64B
// per-slot 描述符:Set0(UBO+实例 SSBO)与 Set2(阴影图采样)
std::vector<vk::DescriptorSet> uboSets;
std::vector<vk::DescriptorSet> shadowSets;
// 材质预览锁定环境集(Set2 同布局,editor 专属——HELLO_EDITOR
// 裁剪):级联/场景色副本随 BindShadowMaps/BindSceneColor 与
// shadowSets 同步重绑(几何资源,无环境语义);IBL 三件套只在
// LockPreviewEnvironment 绑定一次后锁定——材质球观察条件不随
// 场景 SetEnvironment 漂移(UE ThumbnailRenderer 语义)
#ifdef HELLO_EDITOR
std::vector<vk::DescriptorSet> previewEnvSets;
#endif
// 阴影采样器:clamp-to-border 白色 = 光视锥外自动无阴影
//(省 shader 出界分支)
vk::Sampler shadowSampler;
// 场景色副本采样器(binding 6):普通线性 clamp
//(阴影采样器带硬件比较,不可用于常规 sampler2D)
vk::Sampler sceneSampler;
// ---- MDI per-slot ring ----
// 布局:[main | cascade0 | cascade1 | cascade2],每区
// kMaxInstances 条命令预算(每实例至多 1 条命令,必不越界)。
// count buffer 同分区(每组一个 u32,下标 = 组首命令下标)
std::array<Scope<Buffer>, kMaxFramesInFlight> indirectCmdBuffers;
std::array<Scope<Buffer>, kMaxFramesInFlight> indirectCountBuffers;
// shader 引用计数句柄(析构自动销毁,零手工清理)
ShaderManager::ShaderRef forwardVert, forwardFrag;
// 透明变体:同 simple_shader + gl_FragDepth=1.0(见 frag 注释)
ShaderManager::ShaderRef translucentFrag;
ShaderManager::ShaderRef shadowVert, shadowFrag;
// RG 重编译后重绑阴影图(物理镜像按槽位各一份)
void BindShadowMaps(RG::RenderGraph& rg,
const std::array<RG::TextureHandle, kMaxCascades>& maps);
// RG 重编译后重绑场景色副本(Set2 binding 6,透射采样源)
void BindSceneColor(RG::RenderGraph& rg, RG::TextureHandle sceneColor);
// 环境辐照度(Set2 binding 3):view 全程稳定,绑一次即可
void BindEnvironment(const Cubemap& env);
// 材质预览锁定环境:只绑 previewEnvSets 的 IBL 三件套
//(binding 3/4/5),一次调用永久生效;须在 Renderer 初始化
//(环境尚为中性兜底)时调用
void LockPreviewEnvironment(const Cubemap& env);
private:
Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_Descriptors;
};
} // namespace Passes
} // namespace Engine
#endif //HELLOVULKAN_SCENEGPU_H

Some files were not shown because too many files have changed in this diff Show More