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:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user