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
This commit is contained in:
2026-10-07 01:50:04 +08:00
parent a941207ae3
commit cc837bc605
71 changed files with 8198 additions and 548 deletions
@@ -0,0 +1,59 @@
//
// 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 <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);
} // 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,97 @@
//
// 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 特例,同一套
// 加权混合数学,消掉历史双份代码)
void Evaluate(const Model& model, const AnimationClip& cur,
f32 curTime, const CrossfadeState& xf,
std::vector<glm::mat4>& outGlobals);
// 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);
// 绑定姿势:全节点 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,71 @@
//
// HumanoidRig:Unity Avatar 语义的跨骨架重定向映射。
//
// 资产组装现实:角色模型与动画 clip 是两套独立资产,骨骼命名完全
// 不同源(英文/Mixamo/UE/Blender/MMD 日文)。Unity Humanoid 的解法
// 是把任意命名骨架映射到标准人形槽位(Avatar bone mapping),动画
// 经槽位转换后即可驱动任意人形角色。
//
// 这里实现其中枢:名字归一化 → 槽位识别(关键词表,en/Mixamo/MMD)
// → 源/目标骨架间生成 node 下标映射对。同名直映是映射的特例。
//
#ifndef HELLOVULKAN_HUMANOIDRIG_H
#define HELLOVULKAN_HUMANOIDRIG_H
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Model.h>
#include <string_view>
#include <utility>
#include <vector>
namespace Engine::Animation {
// 标准人形槽位(最小主干集,不含手指/表情——主干驱动即可行走)
enum class HumanoidSlot : u32 {
Hips = 0,
Spine,
Chest,
UpperChest,
Neck,
Head,
LeftShoulder,
LeftUpperArm,
LeftLowerArm,
LeftHand,
RightShoulder,
RightUpperArm,
RightLowerArm,
RightHand,
LeftUpperLeg,
LeftLowerLeg,
LeftFoot,
RightUpperLeg,
RightLowerLeg,
RightFoot,
Count
};
// 槽位侧别(左右对称骨骼的识别结果)
enum class Side : u8 { None, Left, Right };
// 槽位显示名("Hips"/"LeftUpperArm"...;调试映射表用)
const char* HumanoidSlotName(HumanoidSlot slot);
// 名字归一化 + 槽位识别。
// "mixamorig:LeftForeArm" → {LowerArm, Left};"左ひじ" → {LowerArm, Left};
// "Upper Arm_L" → {UpperArm, Left}。识别失败 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);
} // namespace Engine::Animation
#endif // HELLOVULKAN_HUMANOIDRIG_H
+67 -6
View File
@@ -19,8 +19,11 @@
#include <Engine/Core/Types.h>
#include <Engine/Core/Log.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>
@@ -109,6 +112,14 @@ namespace Engine
m_Free.push_back(h.index);
}
// 按路径卸载(未加载为 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;
@@ -183,14 +194,19 @@ namespace Engine
// 重映射进目标模型的 clip 表。前提 = 源/目标骨骼命名同源(如
// 同一 DCC 拆分/Mixamo 同角色);名字对不上的轨道跳过并计数。
// 幂等:目标 clips 已含该源(sourcePath)时不重复并入。
struct AnimAttachResult
{
u32 clipsAdded = 0;
u32 channelsMatched = 0;
u32 channelsSkipped = 0; // 骨骼名在目标模型找不到
};
// 本方法只是 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 为半边长。带缓存,同参数共享一份。
ModelRef CreatePlane(f32 halfSize, u8 r, u8 g, u8 b);
@@ -217,6 +233,49 @@ namespace Engine
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 模式:中央注册表)----
@@ -245,6 +304,8 @@ namespace Engine
AssetCache<Model> m_Models;
AssetCache<Cubemap> m_Environments;
AssetCache<MaterialAsset> m_Materials;
AssetCache<Animation::AnimationController> m_Controllers;
// GUID 注册表(双向);GetOrCreateGUID 标 dirty,FlushGUIDRegistry 落盘
// (Application 退出 / 场景保存时统一 flush,避免每次新增都全量重写 yaml)
@@ -19,6 +19,7 @@
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Model.h>
#include <Engine/Renderer/MaterialAsset.h>
#include <Engine/Renderer/Cubemap.h>
namespace Engine::Assets {
@@ -47,6 +48,17 @@ namespace Engine::Assets {
// 异步加载路径:解码已在后台线程完成,此调用只做 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
+2
View File
@@ -22,6 +22,7 @@ namespace Engine {
class Cubemap;
class Texture;
class Material;
class MaterialAsset;
}
namespace Engine {
@@ -41,6 +42,7 @@ namespace Engine {
// 常用别名(新增资产类型时在这里加一行)
using ModelRef = AssetRef<Model>;
using EnvRef = AssetRef<Cubemap>; // 环境(IBL 四件套)
using MaterialAssetRef = AssetRef<MaterialAsset>; // 材质资产(.material)
} // namespace Engine
+86 -9
View File
@@ -21,6 +21,7 @@
#include <cmath>
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
namespace Engine {
@@ -192,45 +193,121 @@ namespace Engine::ECS {
// 网格渲染体:引用共享的 Model 资产。
// 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)——
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
//
// 材质覆盖(UE Material Slot 语义):key = mesh 下标(Model::GetMeshes
// 序),value = .material 资产路径;无该项 = 用模型内建材质(只读)。
// 存路径不存句柄:覆盖材质由渲染收集期按路径惰性 resolve(资产
// 已被场景预载/编辑器指派,收集只查缓存),组件保持纯数据。
// 资产被多实体共享时编辑同时生效;实体独立 = 每实体一份覆盖文件
struct MeshRendererComponent {
ModelRef model;
bool active = true; // 禁用 = 渲染/拾取跳过(Unity enabled / UE SetActive)
std::unordered_map<u32, std::string> materialOverrides;
};
// 骨骼动画播放状态(纯数据 + 便捷控制)。
// 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者);
// ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。
// 骨骼动画播放状态(纯数据 + 便捷控制)——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;
// ---- 播放头 / crossfade(瞬态,不落盘——重启从 0 重播)----
f32 time = 0.0f; // 当前 clip 播放头(秒,wrap 由消费端处理)
// ---- crossfade(clip 切换平滑过渡)----
// 来源 clip 淡出期间与目标 clip 并行采样,局部 TRS 层混合
//(palette 数量不变,只是求值端多采一组姿态)
// crossfade(clip 切换平滑过渡):来源 clip 淡出期间与目标
// clip 并行采样,局部 TRS 层混合(palette 数量不变,只是求值
// 端多采一组姿态)
int fadeFrom = -1; // 来源 clip 下标(-1 = 无过渡)
f32 fadeFromTime = 0.0f; // 来源 clip 播放头
f32 fadeElapsed = 0.0f; // 已过渡时长
f32 fadeDuration = 0.3f; // 过渡总时长(秒)
// ---- 资产引用(序列化落盘)----
// 外部动画资产(编辑器 attach 的动画容器文件,随场景持久化)。
// clips 本体运行时并入 Model 资产(共享);重启后由 Inspector
// 按此列表幂等重放
std::vector<std::string> attachedAnimPaths;
// 切换 clip(带 crossfade)。越界/空 clip 保护在消费端。
// ---- 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; // 消费即清(瞬态)
// 状态机瞬态(不落盘,重启从 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;
}
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) return;
if (index == clipIndex) {
playing = true; // 重复指派 = 确保在播
return;
}
fadeFrom = clipIndex;
fadeFromTime = time;
fadeElapsed = 0.0f;
fadeDuration = fadeSeconds;
clipIndex = index;
time = 0.0f;
playing = true;
}
};
@@ -390,7 +467,7 @@ namespace Engine::ECS {
// 环境光(UE SkyLight 语义:全局漫射环境照明)。多实例取第一个 active
struct SkyLightComponent {
bool active = true;
glm::vec4 ambientColor{0.07f, 0.07f, 0.09f, 0.0f}; // 旧 Settings 默认
glm::vec4 ambientColor{1.0f}; // 旧 Settings 默认
};
} // namespace Engine::ECS
+15
View File
@@ -48,6 +48,18 @@ namespace Engine {
}
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 之间,
@@ -72,6 +84,9 @@ namespace Engine {
vk::DescriptorPool m_Pool;
bool m_Enabled = true;
ImTextureID m_SceneTextures[kMaxFramesInFlight]{};
#ifdef HELLO_EDITOR
ImTextureID m_PreviewTextures[kMaxFramesInFlight]{};
#endif
};
} // namespace Engine
+42 -2
View File
@@ -28,8 +28,22 @@ namespace Engine {
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:
// 投影模式(UE/Unity Camera 语义:透视 = 近大远小;正交 =
// 平行投影,工程图纸/2D/等距视角)
enum class ProjectionMode : u8 { Perspective = 0, Orthographic = 1 };
// 输入驱动(键鼠查询统一走 Input 门面,零 Window 依赖)。
// flyRequiresRMB=true(编辑器模式):移动键仅在 RMB 按住时生效
// ——解决与 Gizmo W/E/R 工具热键的键位冲突(UE 语义:飞行时
@@ -44,8 +58,27 @@ namespace Engine {
// 相机视图 = 实体世界矩阵的逆(要求 transform 已 Compose)
glm::mat4 GetView(const ECS::TransformComponent& transform) const;
// Vulkan 投影:内部完成 Y 翻转(clip space Y 向下)和 [0,1] 深度
glm::mat4 GetProjection(f32 aspect, f32 nearZ, f32 farZ) const;
// 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; }
@@ -63,6 +96,13 @@ namespace Engine {
f32 m_MouseSensitivity = 0.1f;
bool m_MouseLookEnabled = true;
// ---- 投影参数(每实体;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};
+42
View File
@@ -26,6 +26,7 @@
#define HELLOVULKAN_MATERIAL_H
#include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h>
#include <glm/glm.hpp>
#include <vulkan/vulkan.hpp>
#include <array>
@@ -42,11 +43,31 @@ namespace Engine {
// 材质因子(乘贴图;无贴图时即最终值)
// 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 镂空开关
glm::vec4 factors{0.0f, 0.5f, 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)
glm::vec4 misc{0.0f, 0.0f, 0.0f, 0.0f};
// 每槽 KHR_texture_transform(glTF2 每个 textureInfo 独立声明
// offset/scale/rotation——烘焙单份顶点 UV 无法表达"一材质多槽
// 各异变换",收敛为 shader 侧线性式 uv' = A·uv + t,作用在本引擎
// v-up 顶点 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] = {};
};
class Material {
@@ -79,16 +100,37 @@ namespace Engine {
// 透明队列归属(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:
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,96 @@
//
// 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 内嵌纹理无独立文件路径 → 对 glb 模型 Create Override 时
// 纹理槽为空(回退默认纹理),参数/透明照常可编辑。
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 调用):ownedTextures =
// 本资产加载的纹理(独占所有权),material = 组装好的材质
//(空槽已由 uploader 绑共享默认纹理)
MaterialAsset(MaterialAssetData&& data,
std::array<Scope<Texture>, Material::kSlotCount>&& ownedTextures,
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;
// 本资产独占的纹理(共享默认纹理不在其中,由 uploader 持有)
std::array<Scope<Texture>, Material::kSlotCount> m_OwnedTextures;
Scope<Material> m_Material;
};
} // namespace Engine
#endif //HELLOVULKAN_MATERIALASSET_H
+48
View File
@@ -13,6 +13,7 @@
#include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp>
#include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用)
#include <algorithm>
#include <array>
#include <filesystem>
#include <string>
@@ -43,6 +44,8 @@ namespace Engine
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 的物理数据源与
@@ -51,6 +54,18 @@ namespace Engine
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; };
@@ -99,6 +114,10 @@ namespace Engine
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 导致动画源文件在缓存层被整体拒载
@@ -145,6 +164,11 @@ 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; }
@@ -162,6 +186,19 @@ namespace Engine
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; }
@@ -191,6 +228,8 @@ namespace Engine
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,
@@ -198,6 +237,8 @@ namespace Engine
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 权重灌进顶点 + 模型级骨骼表注册去重
@@ -207,6 +248,10 @@ namespace Engine
std::unordered_map<std::string, u32>& boneSlots);
void InitDefaultTextures();
// GLB 内嵌贴图字节("*N" → 编码数据):解析期拷出,纹理解析期
// GetOrLoadTexture 消费(随实例生命周期,键同 texPaths 引用)
std::unordered_map<std::string, std::vector<u8>> m_EmbeddedImages;
// 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除)
void RebuildUnionBounds();
@@ -236,6 +281,9 @@ namespace Engine
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
@@ -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
@@ -20,16 +20,19 @@ namespace Engine::Passes {
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_ShadowMaps(shadowMaps) {}
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 绑定 gl_FragDepth=1.0 的透明 frag 变体;
// translucent=true 绑定透射变体(折射采样场景色副本);
// 深度写与混合状态由各渲染实例作为初始态设置,见 ForwardPass.cpp
void RecordSharedState(const RG::RenderGraphContext& ctx,
vk::CommandBuffer cmd, bool translucent) const;
@@ -39,6 +42,7 @@ namespace Engine::Passes {
RG::TextureHandle m_HDRColor;
RG::TextureHandle m_Velocity; // TAA 运动向量(第二颜色附件)
RG::TextureHandle m_Depth;
RG::TextureHandle m_SceneColor; // 场景色副本(透射折射采样源)
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps;
};
@@ -30,6 +30,7 @@ namespace Engine {
class Device;
class ShaderManager;
class DescriptorManager;
class UniformBuffer;
namespace Passes {
@@ -54,6 +55,13 @@ namespace Engine {
//(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;
@@ -67,6 +75,15 @@ namespace Engine {
// 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
@@ -26,10 +26,14 @@ namespace Engine {
// 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 backbuffer, RG::TextureHandle presentTarget,
RG::TextureHandle previewAtlas)
: m_Layer(layer), m_Frame(frame), m_Backbuffer(backbuffer),
m_PresentTarget(presentTarget) {}
m_PresentTarget(presentTarget), m_PreviewAtlas(previewAtlas) {}
void Register(RG::RenderGraph& rg) override;
@@ -38,6 +42,7 @@ namespace Engine {
FrameSceneData& m_Frame; // displayMode(帧契约)
RG::TextureHandle m_Backbuffer;
RG::TextureHandle m_PresentTarget;
RG::TextureHandle m_PreviewAtlas;
};
} // namespace Passes
@@ -0,0 +1,279 @@
//
// 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(Material* 来自本帧 UI 请求,活指针)
struct PreviewDirtyTile {
Material* material = nullptr;
u32 tile = 0;
};
// 编辑器 tile 池(Renderer 持有,UE FAssetThumbnailPool 语义)。
// 缓存键 = Material serial(出生唯一号,防指针地址复用的 ABA)
// + editVersion(参数代数)。游戏构建整体编译裁剪,无运行时成本
class PreviewTilePool {
public:
// 每帧渲染预算:脏 tile 多时逐帧消化(滚动列表渐进出图)
static constexpr u32 kPreviewRendersPerFrame = 2;
// 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;
}
// 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 段换入,只读)
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);
~MaterialPreviewPass() override = default;
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;
// ---- 预览专用 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
@@ -57,7 +57,8 @@ namespace Engine {
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}
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
@@ -65,10 +66,23 @@ namespace Engine {
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 条命令,必不越界)。
@@ -86,9 +100,17 @@ namespace Engine {
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;
+59
View File
@@ -23,6 +23,7 @@
#include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/Renderer/Passes/MaterialPreviewPass.h> // PreviewTilePool/PreviewDirtyTile(完整类型:成员 vector)
#include <Engine/RenderGraph/RenderGraph.h>
#include <vulkan/vulkan.hpp>
#include <array>
@@ -54,6 +55,7 @@ namespace Engine {
namespace Passes {
class RenderPass; class SceneGpu; class FullscreenGpu;
class PostProcessParams; class PhysicsDebugPass; class HitProxyPass;
class EditorGizmoPass;
}
class Renderer {
@@ -98,6 +100,19 @@ namespace Engine {
// 渲染分辨率(PresentTarget 尺寸,等比适配用)
vk::Extent2D GetRenderExtent() const { return m_Extent; }
// ---- 材质球预览(editor 专属,HELLO_EDITOR 裁剪;game 构建无此 API)----
// UI 构建段只对可见行发幂等请求(UE FAssetThumbnailPool 语义):
// tile 池按 Material serial+参数版本判缓存,渲染一次跨帧持久,
// 脏 tile 走每帧预算。返回 tile:>=0 = 内容可显示(UV 子矩形取
// 图);-1 = 首渲未完成/池满(UI 显示占位)
#ifdef HELLO_EDITOR
i32 RequestMaterialPreview(Material* material);
// 材质实例销毁/重建前调用(ReloadMaterialAsset 等):清池内
// 脏表引用与槽位——脏 tile 本帧不再解引用已销毁材质(UAF)
void DropMaterialPreview(const Material* material);
void* GetMaterialPreviewTexture() const;
#endif
// ---- 后处理参数(ImGui 可调,转发到 pass 共享的 PostProcessParams)----
f32 GetExposure() const;
void SetExposure(f32 e);
@@ -118,6 +133,15 @@ namespace Engine {
void SetBloomEnabled(bool b);
bool GetLodEnabled() const;
void SetLodEnabled(bool b); // 关 = 全部强制 LOD0
#ifdef HELLO_EDITOR
// ---- 骨骼可视化(转发 SceneRenderer;编辑形态专属)----
bool GetBoneWiresVisible() const;
void SetBoneWiresVisible(bool b);
bool GetBoneAllNodes() const;
void SetBoneAllNodes(bool b);
f32 GetBoneJointPx() const;
void SetBoneJointPx(f32 px);
#endif
int GetDebugView() const;
void SetDebugView(int v); // Passes::DebugView 枚举值
// 调试视图名表(下标 = DebugView 枚举值,ImGui combo 直接枚举)
@@ -170,6 +194,11 @@ namespace Engine {
// 不能拿位值兼作哨兵)
void SetSelectedEntity(std::optional<u32> entityBits);
// ---- 帧截图(调试 / 无 UI 自动化验证)----
// 请求 delayFrames 帧后把 PresentTarget(tonemap+outline 后最终
// 画面)拷出存 PNG。触发帧内部 waitIdle;仅调试用途
void RequestScreenshot(const std::string& path, u32 delayFrames = 60);
// 垂直同步开关(present mode 切换会重建 swapchain),
// 只应在帧 UI 构建段调用(Render 之前)
void SetVSync(bool vsync) { m_DesiredVSync = vsync; } // 帧首同步生效
@@ -207,6 +236,14 @@ namespace Engine {
// UniformBuffer——PhysicsDebugPass::PrepareFrame 上传,pass 内绑定)
Scope<UniformBuffer> m_PhysicsLineVB;
Scope<UniformBuffer> m_PhysicsTriVB;
// Editor gizmo 图元 ring VB(editor 专属,game 构建不分配):
// 图标 billboard / 视锥线框 / 拾取代理壳三流——代理壳由
// HitProxyPass 经 AttachGizmoProxy 消费(同 VB 换 shader 重画)
#ifdef HELLO_EDITOR
Scope<UniformBuffer> m_GizmoIconVB;
Scope<UniformBuffer> m_GizmoWireVB;
Scope<UniformBuffer> m_GizmoProxyVB;
#endif
// 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造
Scope<IblGenerator> m_Ibl;
// 中性兜底环境(IBL 四件套):Application 未加载/加载失败时保底;
@@ -230,6 +267,7 @@ namespace Engine {
RG::TextureHandle m_Depth{};
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps{}; // CSM 级联
RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一
RG::TextureHandle m_SceneColor{}; // 不透明段快照(透射折射采样源)
RG::TextureHandle m_Velocity{}; // Forward 输出(clip 空间运动向量)
RG::TextureHandle m_HitProxyId{}; // 拾取 id 目标(R32Uint,片元写实体位模式)
RG::TextureHandle m_SelectionMask{}; // 选中描边 mask(R8Unorm,选中像素=1)
@@ -237,6 +275,9 @@ namespace Engine {
RG::TextureHandle m_BloomA{}; // 半分辨率:提取 + 垂直模糊结果
RG::TextureHandle m_BloomB{}; // 半分辨率:水平模糊中间结果
RG::TextureHandle m_TemporalDiff{}; // 半分辨率:帧历史差异(调试)
RG::TextureHandle m_PreviewHDR{}; // 材质球 atlas(线性 HDR,固定 1024²)
RG::TextureHandle m_PreviewVelocity{}; // 材质球 velocity 附件(forward.frag 双输出契约,无人读)
RG::TextureHandle m_PreviewSDR{}; // 材质球 atlas(tonemap 输出,ImGui 采样)
// pass 共享的可调参数(Renderer 持有,App 层访问器转发)
Scope<Passes::PostProcessParams> m_PostParams;
@@ -249,6 +290,16 @@ namespace Engine {
const PhysicsDebugFrame* m_PhysicsDebug = nullptr;
// HitProxy 拾取(非拥有指针:pass 在 m_Passes 内)
Passes::HitProxyPass* m_HitProxyPass = nullptr;
// 材质球预览(editor 专属;HELLO_EDITOR 裁剪,game 构建零预览
// 代码路径——pass 不创建、atlas 不创建、请求池不存在)
#ifdef HELLO_EDITOR
// 非拥有指针:pass 在 m_Passes 内,构造时持有 m_PreviewRenders 引用
Passes::MaterialPreviewPass* m_PreviewPass = nullptr;
// tile 池:UI 段 Request 标脏(收集活指针),Render 段
// TakeRenders 取预算内的脏 tile 换入 m_PreviewRenders(pass 只读)
Passes::PreviewTilePool m_PreviewPool;
std::vector<Passes::PreviewDirtyTile> m_PreviewRenders;
#endif
// GPU timestamp(pass 级耗时,ImGui 可读)
vk::QueryPool m_QueryPool;
u32 m_QueriesPerFrame = 0; // 每帧槽位的查询数 = pass 数 × 2
@@ -266,6 +317,14 @@ namespace Engine {
// ---- 帧首同步的意图缓冲(UI 段写入,下一帧帧首落定)----
int m_DesiredDisplayMode = kDisplayDirect; // 默认直接渲染, 视口渲染需要手动指定一下
vk::Extent2D m_DesiredViewport{}; // Editor 视口渲染分辨率(面板上报)
// 截图意图:framesLeft 递减到 0 的那一帧末尾拷贝 PresentTarget 存 PNG
struct ScreenshotRequest
{
std::string path;
u32 framesLeft = 0;
};
std::optional<ScreenshotRequest> m_PendingShot;
bool m_DesiredVSync = true;
};
+53 -12
View File
@@ -23,11 +23,13 @@
#include <Engine/Core/Types.h>
#include <Engine/Renderer/Mesh.h>
#include <Engine/Renderer/SceneTypes.h>
#include <Engine/Animation/AnimationSystem.h>
#include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp>
#include <array>
#include <optional>
#include <unordered_map>
#include <unordered_set>
#include <Engine/Renderer/EditorCamera.h>
@@ -70,6 +72,15 @@ namespace Engine {
// primary 相机渲染进 sceneTex(Play 后 Scene view 显示游戏画面,
// 编辑器 UI 保持可用)。kDisplayDirect 恒为游戏形态不受此影响
void SetGameView(bool g) { m_GameView = g; }
#ifdef HELLO_EDITOR
// ---- 骨骼可视化设置(Editor 面板 → Renderer 门面 → 落帧)----
void SetBoneWiresVisible(bool b) { m_ShowBoneWires = b; }
void SetBoneAllNodes(bool b) { m_BoneAllNodes = b; }
void SetBoneJointPx(f32 px) { m_BoneJointPx = px; }
bool GetBoneWiresVisible() const { return m_ShowBoneWires; }
bool GetBoneAllNodes() const { return m_BoneAllNodes; }
f32 GetBoneJointPx() const { return m_BoneJointPx; }
#endif
// 编辑器相机(视口 UI 状态,SceneRenderer 持有 = 渲染源在此分派);
// Application 驱动 Update,ViewportPanel/Inspector 经 Renderer 门面访问
EditorCamera& GetEditorCamera() { return m_EditorCamera; }
@@ -88,8 +99,24 @@ namespace Engine {
private:
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
//(含纹素对齐防阴影边缘闪烁)。写入(Shadow pass push)与采样
//(Forward UBO)共用同一组矩阵。
void ComputeCascades(const glm::vec3& shadowDir);
//(Forward UBO)共用同一组矩阵。camNear/camFar 取自当帧渲染源
// 相机(每实体参数)
void ComputeCascades(const glm::vec3& shadowDir,
f32 camNear, f32 camFar);
#ifdef HELLO_EDITOR
// Editor gizmo 可视化收集(billboard 图标 / 相机视锥线框 /
// HitProxy 代理壳)——只读场景写 FrameSceneData editor 段。
// camPos/right/up 供 billboard 面向展开;aspect 供视锥角点;
// viewHeightPx 供代理 ribbon 恒定像素屏宽换算;viewTanHalf 为
// 编辑器视角相机的 tan(fov/2)(billboard 尺寸基准)
void CollectEditorGizmos(ECS::Scene& scene,
const glm::vec3& camPos,
const glm::vec3& camRight,
const glm::vec3& camUp,
f32 aspect, f32 viewHeightPx,
f32 viewTanHalf);
#endif
// MDI 打包:batch 按组键排序(megabuffer + 可选材质)后压成
// IndirectDrawCmd 连续数组 + IndirectGroup 区间表。
@@ -128,6 +155,13 @@ namespace Engine {
// batch 查找:数组寻址(下标 = Mesh::GetSlot(),值 = 每 LOD 的
// batchIdx+1,0 = 未用)——逐实例 hash 在大场景是毫秒级开销
std::vector<std::array<u32, kMeshLodCount>> lookup;
// 覆盖材质的 batch 查找(内建材质走 lookup 数组快路径):
// key = meshSlot<<32 | matVariant(matVariant = 覆盖材质的
// 帧内序号,指针仅帧内有效),value = 每 LOD 的 batchIdx+1
std::unordered_map<u64, std::array<u32, kMeshLodCount>> ovrLookup;
// 帧内覆盖材质 → 序号分配表(指针仅帧内有效——材质资产
// Reload 会在帧间换代,禁止跨帧缓存)
std::unordered_map<Material*, u32> matVariants;
void Clear()
{
@@ -138,6 +172,8 @@ namespace Engine {
batchHead.clear();
batchDistSum.clear();
for (auto& e : lookup) e.fill(0); // 容量保留,置零复用
ovrLookup.clear();
matVariants.clear();
}
};
ViewScratch m_MainScratch;
@@ -155,16 +191,11 @@ namespace Engine {
std::vector<std::pair<u32, glm::mat4>> m_PrevWorlds;
std::vector<std::pair<u32, glm::mat4>> m_CurWorlds;
// ---- 骨骼动画 scratch(帧首重建/容量复用)----
// entKey → palette 区段基址(第 2.5 节预采样填,收集段查)
std::unordered_map<u32, u32> m_EntitySkinBase;
// 采样层 A(当前 clip)/ B(fade 来源 clip)与 global 累乘
std::vector<glm::vec3> m_PosePosA, m_PosePosB;
std::vector<glm::quat> m_PoseRotA, m_PoseRotB;
std::vector<glm::vec3> m_PoseSclA, m_PoseSclB;
std::vector<u8> m_TouchedA, m_TouchedB;
std::vector<glm::mat4> m_Globals;
bool m_SkinTruncWarned = false; // palette 预算溢出只告警一次
// ---- 骨骼动画(调度收口 Animation::AnimationSystem,本类只
// 转发调用 + 收集段只读查 frame.entitySkinBases)----
Animation::AnimationSystem m_AnimSystem;
// 材质资产加载缺失的一次性告警节流(按路径;发现即插表)
std::unordered_set<std::string> m_WarnedMaterialMisses;
bool m_TruncationWarned = false; // 实例缓冲打满只告警一次
@@ -176,6 +207,16 @@ namespace Engine {
int m_DebugView = 0;
int m_DisplayMode = kDisplayViewport; // Editor 视口为默认形态
bool m_GameView = false; // PIE 内嵌播放(Editor Play/Stop 驱动)
#ifdef HELLO_EDITOR
bool m_GizmoTruncWarned = false; // gizmo 顶点预算溢出只告警一次
// 骨骼可视化设置(唯一持有者,BuildFrame 落帧)。默认关闭:
// 骨架是调试可视化(Inspector Animation 节手动打开),常显
// 会污染编辑视图
bool m_ShowBoneWires = false;
bool m_BoneAllNodes = false;
f32 m_BoneJointPx = 5.0f;
#endif
};
} // namespace Engine
+76 -2
View File
@@ -13,6 +13,7 @@
#include <Engine/Core/Types.h>
#include <glm/glm.hpp>
#include <unordered_map>
#include <vector>
namespace Engine {
@@ -20,10 +21,13 @@ namespace Engine {
class Mesh;
class Material;
// GPUDriven:按 (mesh, LOD) 聚合的绘制批——同 mesh 不同 LOD 的实例
// 索引区间不同,不能并成一次 draw
// GPUDriven:按 (mesh, 材质, LOD) 聚合的绘制批——同 mesh 不同 LOD
// 的实例索引区间不同不能并成一次 draw;同 mesh 不同材质(实体级
// 覆盖)描述符集不同,同样必须分批
struct InstanceBatch {
Mesh* mesh;
Material* material = nullptr; // 收集期 resolve 的实际材质
//(覆盖材质或 mesh 内建;shadow 组不绑材质但同表记录)
u32 first; // 在实例矩阵数组中的起始下标
u32 count; // 实例数
u32 lod = 0; // 绘制用索引区间(Mesh::Draw)
@@ -92,6 +96,47 @@ namespace Engine {
// 位模式,永不与有效实体冲突
inline constexpr u32 kPickMiss = 0xFFFFFFFFu;
#ifdef HELLO_EDITOR
// ---- Editor gizmo 可视化契约(SceneRenderer 生产,EditorGizmoPass /
// HitProxyPass 消费;game 构建整段不存在)----
// 顶点预算:图标 ≈680 个(4096/6),线框覆盖 ~200 相机视锥 + 半径圆
inline constexpr u32 kMaxGizmoIconVerts = 4096;
inline constexpr u32 kMaxGizmoWireVerts = 16384;
inline constexpr u32 kMaxGizmoProxyVerts = 32768;
// billboard/拾取代理 quad 顶点(48B):pos 已由收集期面向相机展开,
// aux.x = 形状 id(shader SDF 分支),aux.yz = quad 本地 uv(-1..1)
struct GizmoQuadVertex {
glm::vec3 pos;
glm::vec4 color; // rgb 线性 HDR 主色,a = 不透明度
glm::vec4 aux;
u32 entityId; // HitProxy 位模式(拾取 shader 顶点直读)
};
static_assert(sizeof(GizmoQuadVertex) == 48);
// 线框顶点(28B):布局与 debug_line.vert 输入同构 → 复用其 shader
struct GizmoWireVertex {
glm::vec3 pos;
glm::vec4 color;
};
static_assert(sizeof(GizmoWireVertex) == 28);
// 选中高亮色(与 OutlinePass 描边同款,UE 语义统一)。gizmo 收集
// 与 Animation 骨骼可视化共用一份定义
inline const glm::vec4 kGizmoSelectedColor{1.0f, 0.62f, 0.05f, 1.0f};
// 非骨骼节点的暗色(骨骼线亮橙 / 普通节点线暗灰,层级一眼分层;
// LineList 无混合,alpha 无效,只能实色区分)
inline const glm::vec4 kGizmoNodeDimColor{0.42f, 0.42f, 0.46f, 1.0f};
// 骨骼关节标记(AnimationSystem 产出,ViewportPanel 投影到屏幕画
// 圆点 + tooltip 骨骼名——Endfield-Poser overlay 范式)
struct BoneJointMark {
glm::vec3 pos; // 关节世界坐标
u32 nodeIdx; // 查 Model::GetNodes()[nodeIdx].name 用
u8 isBone; // 1 = 骨骼(bone 表成员),0 = 普通节点
};
#endif
// 点击拾取请求(UI 段写入 → SceneRenderer 落帧 → HitProxy pass 只读)
struct PickRequest {
bool active = false;
@@ -162,6 +207,10 @@ namespace Engine {
// palette 区段池:帧首 bump allocator 按动画实体分配
// [skinBase .. skinBase+boneCount) = 该实体全部骨骼的蒙皮矩阵
std::vector<glm::mat4> skinPalettes;
// 实体(entt integral)→ 该实体 palette 区段基址。AnimationSystem
// 预采样填,收集段查表(查不到 = 无动画/组件禁用 → identity 槽
// = 绑定姿势)。渲染侧对动画零逻辑,只消费查表结果
std::unordered_map<u32, u32> entitySkinBases;
// per-instance 实体 id(与 instanceModels 平行对齐;
// HitProxy id pass 按 gl_InstanceIndex 取,片元写入 R32Uint 目标)
@@ -203,6 +252,31 @@ namespace Engine {
// palette 预算溢出(kMaxSkinMatrices):动画实体 palette 被截断
bool skinPaletteTruncated = false;
#ifdef HELLO_EDITOR
// ---- Editor gizmo 可视化(收集门控:kDisplayViewport 且非 PIE
// 时非空;每帧 clear() 复用容量,稳态零堆分配)----
std::vector<GizmoQuadVertex> gizmoIconVerts; // 视觉 billboard(三角表)
std::vector<GizmoWireVertex> gizmoWireVerts; // 线段对(LineList)
// 选中实体骨骼线框(AnimationSystem 产出,EditorGizmoPass 与
// gizmoWireVerts 拼同一 LineList 绘制)。动画实体跟随当帧姿态,
// 非动画/禁用实体 = 绑定姿势(与渲染 identity palette 一致)
std::vector<GizmoWireVertex> boneWireVerts;
std::vector<GizmoQuadVertex> gizmoProxyVerts; // HitProxy 代理壳
// ---- 骨骼可视化设置(ImGui 经 Renderer 门面写入)----
bool showBoneWires = true; // 总开关(3D 骨架线 + 关节圆点)
bool pieActive = false; // PIE 运行态(Play/Stop 驱动):骨骼
// 线/关节圆点全部停产出——PIE 画面
// 是 primary 相机渲染的"游戏",编辑器
// 可视化叠上去必然错位
bool boneAllNodes = false; // true = 全节点层级(普通节点暗色);
// false = 仅骨骼链(bone 表成员)
f32 boneJointPx = 5.0f; // 关节圆点半径(屏幕像素,overlay 画)
// 关节标记(EmitSelectedSkeleton 产出,每帧重建;ViewportPanel
// 用编辑器相机投影画 overlay 圆点)
std::vector<BoneJointMark> boneJoints;
#endif
f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测)
};
@@ -40,8 +40,22 @@ namespace Engine {
Scope<Cubemap> LoadEnvironment(const std::string& path) override;
Scope<Cubemap> LoadEnvironment(const std::string& path,
DecodedEquirect&& decoded) override;
// 析构外置(成员持 Scope<Texture> 数组等重资源,遵循
// Model/Material 既有模式)
~VulkanAssetUploader() override;
// ---- 材质资产(.material 数据段 → GPU 段)----
// 每槽路径建独占纹理(空/缺失回退共享默认纹理),再组 Material
Scope<MaterialAsset> CreateMaterial(MaterialAssetData&& data) override;
// 设备空闲屏障(编辑器资产重建路径:卸载旧 GPU 资源前全停飞)
void WaitDeviceIdle() override;
private:
// 共享默认纹理集(全部材质资产共用一份,惰性创建;
// 单点实现 = MaterialAsset.cpp 的 CreateDefaultMaterialTexture)
void InitSharedDefaultTextures();
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures{};
Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_Descriptors;
+7
View File
@@ -35,6 +35,13 @@ namespace Engine {
static Scope<Texture> CreateSolidColorRGBA(Device& device, MemoryAllocator& allocator,
u8 r, u8 g, u8 b, u8 a, bool srgb = true);
// 内存编码图像(PNG/JPEG 原始字节)→ 纹理:GLB 内嵌贴图
//(assimp "*<index>" 约定)的解析端,复用 InitFromPixels
// 公共上传路径。解码失败返回空(调用方折到默认纹理)
static Scope<Texture> CreateFromEncodedMemory(
Device& device, MemoryAllocator& allocator,
const void* data, u32 bytes, bool srgb);
~Texture();
Texture(const Texture&) = delete;
@@ -41,6 +41,11 @@ namespace Engine {
// 写入指定帧槽位的 UBO(仅在该槽位 fence 确认完成后调用)
void Update(u32 frameSlot, const void* data, u64 size) const;
// 偏移写入(同槽位 fence 契约同上)。多段数据共享同一 buffer 时
// 用——gizmo 线流 + 骨骼线流拼同一顶点缓冲
void UpdateAt(u32 frameSlot, u64 offset, const void* data,
u64 size) const;
private:
std::vector<Scope<Buffer>> m_Buffers;
u64 m_Size = 0;