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:
@@ -6,7 +6,13 @@ file(GLOB EDITOR_SOURCES CONFIGURE_DEPENDS
|
||||
file(GLOB SCRIPT_SOURCES CONFIGURE_DEPENDS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/scripts/*.cpp)
|
||||
|
||||
# editor 形态(HELLOVULKAN_EDITOR=ON)编入 editor/(docking 编辑器);
|
||||
# game 形态只有 main + scripts(编辑器源码整体不进产物)
|
||||
if(HELLOVULKAN_EDITOR)
|
||||
add_executable(HelloVulkan main.cpp ${EDITOR_SOURCES} ${SCRIPT_SOURCES})
|
||||
else()
|
||||
add_executable(HelloVulkan main.cpp ${SCRIPT_SOURCES})
|
||||
endif()
|
||||
|
||||
target_include_directories(HelloVulkan PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/editor)
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
//
|
||||
|
||||
#include "AssetBrowserPanel.h"
|
||||
#include "MaterialEditorPanel.h" // kMaterialDragPayload + 双击打开材质编辑器
|
||||
#include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
@@ -23,11 +25,12 @@ namespace {
|
||||
constexpr const char* kRootPaths[] = {"assets", "scenes"};
|
||||
|
||||
// 类型过滤(下标即身份):0 = All
|
||||
constexpr const char* kTypeNames[] = {"All", "Models", "Textures",
|
||||
"Scenes"};
|
||||
constexpr const char* kTypeNames[] = {"All", "Models",
|
||||
"Textures", "Scenes",
|
||||
"Materials", "Controllers"};
|
||||
|
||||
// 资产类别:按扩展名分派(过滤用)
|
||||
enum class Kind { Model, Texture, Scene, Other };
|
||||
enum class Kind { Model, Texture, Scene, Material, AnimClip, Other };
|
||||
|
||||
Kind KindOf(const std::filesystem::path& p)
|
||||
{
|
||||
@@ -48,6 +51,14 @@ namespace {
|
||||
{
|
||||
return Kind::Scene;
|
||||
}
|
||||
if (ext == ".material")
|
||||
{
|
||||
return Kind::Material;
|
||||
}
|
||||
if (ext == ".animctrl")
|
||||
{
|
||||
return Kind::AnimClip;
|
||||
}
|
||||
return Kind::Other;
|
||||
}
|
||||
|
||||
@@ -58,6 +69,8 @@ namespace {
|
||||
case 1: return kind == Kind::Model;
|
||||
case 2: return kind == Kind::Texture;
|
||||
case 3: return kind == Kind::Scene;
|
||||
case 4: return kind == Kind::Material;
|
||||
case 5: return kind == Kind::AnimClip;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
@@ -70,6 +83,8 @@ namespace {
|
||||
case Kind::Model: return "[M]";
|
||||
case Kind::Texture: return "[T]";
|
||||
case Kind::Scene: return "[S]";
|
||||
case Kind::Material: return "[Ma]";
|
||||
case Kind::AnimClip: return "[An]";
|
||||
default: return "[ ]";
|
||||
}
|
||||
}
|
||||
@@ -106,7 +121,7 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx)
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(110.0f);
|
||||
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 4);
|
||||
ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 6);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(140.0f);
|
||||
ImGui::InputTextWithHint("##name", "filter", m_NameFilter,
|
||||
@@ -262,12 +277,41 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
|
||||
ImGui::SameLine();
|
||||
ImGui::Selectable(name.c_str());
|
||||
|
||||
// 双击预热:model/HDR 进缓存(路径去重,重复双击零成本);
|
||||
// 场景加载含环境恢复/选中清理流程,提示走 File 菜单
|
||||
// .material 拖拽源:拖到 Details 材质槽球上指派(kMaterialDragPayload)
|
||||
if (item.kind == Kind::Material && ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(kMaterialDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// .animctrl 拖拽源:拖到 Details Controller 槽指派
|
||||
//(kControllerDragPayload,对齐材质槽交互)
|
||||
if (item.kind == Kind::AnimClip && ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(kControllerDragPayload,
|
||||
assetPath.c_str(), assetPath.size() + 1);
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::EndDragDropSource();
|
||||
}
|
||||
|
||||
// 双击:.material 打开 Material Editor;.animctrl 打开
|
||||
// Controller Editor;model/HDR 进缓存预热(路径去重,重复
|
||||
// 双击零成本);场景加载含环境恢复/选中清理流程,提示走
|
||||
// File 菜单
|
||||
if (ImGui::IsItemHovered() &&
|
||||
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
if (item.kind == Kind::Model && ctx.assets)
|
||||
if (item.kind == Kind::Material && ctx.materialEditor)
|
||||
{
|
||||
ctx.materialEditor->OpenAsset(assetPath);
|
||||
}
|
||||
else if (item.kind == Kind::AnimClip && ctx.controllerEditor)
|
||||
{
|
||||
ctx.controllerEditor->Open(assetPath);
|
||||
}
|
||||
else if (item.kind == Kind::Model && ctx.assets)
|
||||
{
|
||||
const auto ref = ctx.assets->LoadModel(assetPath);
|
||||
ENGINE_INFO("AssetBrowser: model '{}' loaded (valid={})",
|
||||
@@ -292,8 +336,11 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx,
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::Separator();
|
||||
// 与 Kind 枚举逐位对齐(Kind::Other = 5)——漏项 = tooltip
|
||||
// 数组越界读
|
||||
static const char* kKindNames[] = {"Model", "Texture/Environment",
|
||||
"Scene", "File"};
|
||||
"Scene", "Material",
|
||||
"Controller", "File"};
|
||||
ImGui::Text("type: %s", kKindNames[static_cast<int>(item.kind)]);
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
|
||||
@@ -0,0 +1,786 @@
|
||||
//
|
||||
// ControllerEditorPanel 实现:见头文件注释。
|
||||
//
|
||||
|
||||
#include "ControllerEditorPanel.h"
|
||||
|
||||
#include <Engine/Animation/AnimationController.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
|
||||
ImVec4 WarnCol() { return ImVec4(1.0f, 0.75f, 0.2f, 1.0f); }
|
||||
|
||||
// 模型首个 clip 名(children/树新建时的默认引用;无模型 = 空)
|
||||
std::string FirstClipName(const Engine::Model* model)
|
||||
{
|
||||
if (model == nullptr || model->GetClips().empty()) return "";
|
||||
return model->GetClips()[0].name;
|
||||
}
|
||||
|
||||
// clip 引用编辑:选中实体有模型 → combo(模型 clip 列表);
|
||||
// 无模型 → InputText 手填(clip 缺失 = 该层 rest 兜底,不崩)
|
||||
bool ClipRefEdit(const char* label, std::string& clip,
|
||||
const Engine::Model* model)
|
||||
{
|
||||
if (model == nullptr || model->GetClips().empty())
|
||||
{
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "%s", clip.c_str());
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (ImGui::InputText(label, buf, sizeof(buf)))
|
||||
{
|
||||
clip = buf;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::vector<const char*> names;
|
||||
names.reserve(model->GetClips().size());
|
||||
int sel = -1;
|
||||
for (const auto& c : model->GetClips())
|
||||
{
|
||||
names.push_back(c.name.c_str());
|
||||
if (c.name == clip) sel = static_cast<int>(names.size()) - 1;
|
||||
}
|
||||
if (sel < 0) sel = 0;
|
||||
if (ImGui::Combo(label, &sel, names.data(),
|
||||
static_cast<int>(names.size())))
|
||||
{
|
||||
clip = model->GetClips()[static_cast<size_t>(sel)].name;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ControllerEditorPanel::Draw(const EditorContext& ctx)
|
||||
{
|
||||
if (!m_Open || m_Path.empty()) return;
|
||||
|
||||
Engine::Animation::AnimationController* ctrl =
|
||||
ctx.assets->FindController(m_Path);
|
||||
if (!ImGui::Begin("Controller Editor", &m_Open))
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
if (ctrl == nullptr)
|
||||
{
|
||||
ImGui::TextColored(WarnCol(), "'%s' failed to load",
|
||||
m_Path.c_str());
|
||||
if (ImGui::Button("Reload"))
|
||||
{
|
||||
ctx.assets->ReloadController(m_Path);
|
||||
}
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 头部:路径 + Save(落盘 + Reload 重编译条件)----
|
||||
ImGui::TextUnformatted(m_Path.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Save"))
|
||||
{
|
||||
if (ctrl->Save(m_Path))
|
||||
{
|
||||
ctx.assets->ReloadController(m_Path);
|
||||
ctrl = ctx.assets->FindController(m_Path);
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("edits apply live; Save writes the file");
|
||||
ImGui::Separator();
|
||||
|
||||
DrawPreview(ctx);
|
||||
ImGui::Separator();
|
||||
DrawStates(ctx, ctrl);
|
||||
ImGui::Separator();
|
||||
DrawTransitions(ctx, ctrl);
|
||||
ImGui::Separator();
|
||||
DrawParams(ctx, ctrl);
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// ---- Preview:方向盘(拖动 = SetFloat 到选中实体,摇杆回中)----
|
||||
void ControllerEditorPanel::DrawPreview(const EditorContext& ctx)
|
||||
{
|
||||
// 选中实体的 AnimationComponent(Controller 模式才驱动)
|
||||
Engine::ECS::AnimationComponent* anim = nullptr;
|
||||
const Engine::Model* model = nullptr;
|
||||
if (ctx.selected != nullptr && ctx.scene != nullptr &&
|
||||
ctx.scene->GetRegistry().valid(*ctx.selected))
|
||||
{
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
if (auto* a =
|
||||
reg.try_get<Engine::ECS::AnimationComponent>(*ctx.selected))
|
||||
{
|
||||
anim = a;
|
||||
if (auto* mr =
|
||||
reg.try_get<Engine::ECS::MeshRendererComponent>(
|
||||
*ctx.selected))
|
||||
{
|
||||
if (mr->model.Valid())
|
||||
{
|
||||
model = ctx.assets->GetModel(mr->model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (anim == nullptr)
|
||||
{
|
||||
ImGui::TextDisabled("select an entity with an Animation component "
|
||||
"to preview");
|
||||
return;
|
||||
}
|
||||
|
||||
Engine::Animation::AnimationController* ctrl =
|
||||
anim->controllerPath.empty()
|
||||
? nullptr
|
||||
: ctx.assets->FindController(anim->controllerPath);
|
||||
if (ctrl == nullptr) return;
|
||||
|
||||
// 面板选中状态的 2D 树(预览读出;拖动写的是实体参数)
|
||||
const Engine::Animation::ControllerState* padState = nullptr;
|
||||
const auto& order = ctrl->GetStateOrder();
|
||||
if (m_SelState >= 0 && m_SelState < static_cast<int>(order.size()))
|
||||
{
|
||||
const auto& s = ctrl->GetStates().at(order[static_cast<size_t>(
|
||||
m_SelState)]);
|
||||
if (s.tree.type ==
|
||||
Engine::Animation::BlendTree::Type::Directional2D)
|
||||
{
|
||||
padState = &s;
|
||||
}
|
||||
}
|
||||
if (padState == nullptr)
|
||||
{
|
||||
ImGui::TextDisabled("preview: select a 2DDirectional state");
|
||||
return;
|
||||
}
|
||||
|
||||
const float S = 190.0f;
|
||||
const float rad = S * 0.5f - 14.0f;
|
||||
const ImVec2 org = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 ctr(org.x + S * 0.5f, org.y + S * 0.5f);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
dl->AddCircleFilled(ctr, rad, IM_COL32(30, 30, 34, 255));
|
||||
dl->AddCircle(ctr, rad, IM_COL32(90, 90, 100, 255), 48, 1.5f);
|
||||
dl->AddLine(ImVec2(ctr.x - rad, ctr.y), ImVec2(ctr.x + rad, ctr.y),
|
||||
IM_COL32(60, 60, 68, 255));
|
||||
dl->AddLine(ImVec2(ctr.x, ctr.y - rad), ImVec2(ctr.x, ctr.y + rad),
|
||||
IM_COL32(60, 60, 68, 255));
|
||||
const auto& ch = padState->tree.children;
|
||||
for (const auto& c : ch)
|
||||
{
|
||||
const bool isOrigin =
|
||||
c.pos.x * c.pos.x + c.pos.y * c.pos.y < 1e-8f;
|
||||
const ImVec2 p(ctr.x + c.pos.x * rad, ctr.y - c.pos.y * rad);
|
||||
dl->AddCircleFilled(p, isOrigin ? 5.0f : 4.0f,
|
||||
isOrigin ? IM_COL32(90, 140, 255, 255)
|
||||
: IM_COL32(220, 220, 225, 255));
|
||||
}
|
||||
// mode=Direct 的实体不跑状态机——参数写进去没有任何效果,
|
||||
// 直接禁用方向盘(Unity:Animator 组件没挂 Controller 时
|
||||
// 参数面板不可编辑,同语义)
|
||||
const bool isCtrlMode =
|
||||
anim->mode == Engine::ECS::AnimationComponent::AnimMode::Controller;
|
||||
if (!isCtrlMode)
|
||||
{
|
||||
ImGui::TextColored(WarnCol(),
|
||||
"entity is in Direct mode - switch Mode to "
|
||||
"Controller to preview");
|
||||
return;
|
||||
}
|
||||
ImGui::InvisibleButton("##dirpad", ImVec2(S, S));
|
||||
const auto getF = [&](const std::string& n) -> float {
|
||||
auto it = anim->floatParams.find(n);
|
||||
return it != anim->floatParams.end() ? it->second : 0.0f;
|
||||
};
|
||||
float cx = getF(padState->tree.xParam);
|
||||
float cz = getF(padState->tree.zParam);
|
||||
if (ImGui::IsItemActivated())
|
||||
{
|
||||
// 预览闭环(Unity Animator 窗口语义:选中状态即可预览该
|
||||
// 状态)——实体停在别的状态时拖盘自动切到面板选中的 2D
|
||||
// 树状态,否则 SetFloat 对 Single 状态完全无效
|
||||
if (anim->currentState != order[static_cast<size_t>(m_SelState)])
|
||||
{
|
||||
anim->CrossFade(order[static_cast<size_t>(m_SelState)], 0.1f);
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemActive())
|
||||
{
|
||||
const ImVec2 m = ImGui::GetIO().MousePos;
|
||||
float vx = (m.x - ctr.x) / rad;
|
||||
float vz = -(m.y - ctr.y) / rad;
|
||||
const float len = glm::length(glm::vec2(vx, vz));
|
||||
if (len > 1.0f)
|
||||
{
|
||||
vx /= len;
|
||||
vz /= len;
|
||||
}
|
||||
anim->SetFloat(padState->tree.xParam, vx);
|
||||
anim->SetFloat(padState->tree.zParam, vz);
|
||||
cx = vx;
|
||||
cz = vz;
|
||||
}
|
||||
else if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
anim->SetFloat(padState->tree.xParam, 0.0f);
|
||||
anim->SetFloat(padState->tree.zParam, 0.0f);
|
||||
cx = cz = 0.0f;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
const ImVec2 m = ImGui::GetIO().MousePos;
|
||||
for (const auto& c : ch)
|
||||
{
|
||||
const ImVec2 p(ctr.x + c.pos.x * rad, ctr.y - c.pos.y * rad);
|
||||
const float dx = m.x - p.x;
|
||||
const float dy = m.y - p.y;
|
||||
if (dx * dx + dy * dy < 64.0f)
|
||||
{
|
||||
ImGui::SetTooltip("%s", c.clip.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const ImVec2 cur(ctr.x + cx * rad, ctr.y - cz * rad);
|
||||
dl->AddCircleFilled(cur, 6.0f, IM_COL32(255, 170, 60, 255));
|
||||
dl->AddCircle(cur, 9.0f, IM_COL32(255, 170, 60, 160), 0, 1.5f);
|
||||
|
||||
// 右侧读出:当前参数 + 选中状态树的权重(运行时同一数学)
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
ImGui::Text("preview: %s", anim->currentState.c_str());
|
||||
ImGui::Text("%.2f / %.2f", cx, cz);
|
||||
std::vector<std::pair<Engine::u32, float>> ws;
|
||||
Engine::Animation::EvaluateBlendTree2D(padState->tree, cx, cz, ws);
|
||||
std::sort(ws.begin(), ws.end(),
|
||||
[](const auto& a, const auto& b)
|
||||
{ return a.second > b.second; });
|
||||
for (const auto& [idx, w] : ws)
|
||||
{
|
||||
ImGui::Text("%s %.2f", ch[idx].clip.c_str(), w);
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
|
||||
// ---- States:列表(entry ★)+ 选中状态详情 + children 编辑 ----
|
||||
void ControllerEditorPanel::DrawStates(
|
||||
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
|
||||
{
|
||||
if (!ImGui::TreeNodeEx("States", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto& order = ctrl->StateOrderMut();
|
||||
auto& states = ctrl->StatesMut();
|
||||
|
||||
// 选中实体模型(children 的 clip 下拉来源)
|
||||
const Engine::Model* model = nullptr;
|
||||
if (ctx.selected != nullptr && ctx.scene != nullptr &&
|
||||
ctx.scene->GetRegistry().valid(*ctx.selected))
|
||||
{
|
||||
if (auto* mr =
|
||||
ctx.scene->GetRegistry()
|
||||
.try_get<Engine::ECS::MeshRendererComponent>(
|
||||
*ctx.selected))
|
||||
{
|
||||
if (mr->model.Valid())
|
||||
{
|
||||
model = ctx.assets->GetModel(mr->model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 状态列表 + Add
|
||||
int delIdx = -1;
|
||||
for (int i = 0; i < static_cast<int>(order.size()); ++i)
|
||||
{
|
||||
const bool isEntry = order[static_cast<size_t>(i)] ==
|
||||
ctrl->GetEntryState();
|
||||
ImGui::PushID(i);
|
||||
if (ImGui::Selectable(
|
||||
(order[static_cast<size_t>(i)] +
|
||||
std::string(isEntry ? " [entry]" : ""))
|
||||
.c_str(),
|
||||
i == m_SelState))
|
||||
{
|
||||
m_SelState = i;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
{
|
||||
static char s_NewName[64] = "";
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputText("##new_state", s_NewName, sizeof(s_NewName));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+ Add") && s_NewName[0] != '\0' &&
|
||||
states.find(s_NewName) == states.end())
|
||||
{
|
||||
const std::string n = s_NewName;
|
||||
states.emplace(n, Engine::Animation::ControllerState{n});
|
||||
order.push_back(n);
|
||||
m_SelState = static_cast<int>(order.size()) - 1;
|
||||
s_NewName[0] = '\0';
|
||||
}
|
||||
}
|
||||
if (m_SelState >= static_cast<int>(order.size()))
|
||||
{
|
||||
m_SelState = static_cast<int>(order.size()) - 1;
|
||||
}
|
||||
if (m_SelState < 0 || order.empty())
|
||||
{
|
||||
ImGui::TreePop();
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 选中状态详情 ----
|
||||
const std::string selName = order[static_cast<size_t>(m_SelState)];
|
||||
auto it = states.find(selName);
|
||||
if (it == states.end())
|
||||
{
|
||||
ImGui::TreePop();
|
||||
return;
|
||||
}
|
||||
auto& st = it->second;
|
||||
ImGui::Indent();
|
||||
|
||||
ImGui::TextUnformatted("editing:");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(selName.c_str());
|
||||
if (selName != ctrl->GetEntryState() &&
|
||||
ImGui::SmallButton("Set as Entry"))
|
||||
{
|
||||
ctrl->SetEntry(selName);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::PushID("delstate");
|
||||
if (ImGui::SmallButton("x Delete"))
|
||||
{
|
||||
delIdx = m_SelState;
|
||||
}
|
||||
ImGui::PopID();
|
||||
// 重命名(级联:transitions / transitionsOnEnd / entry 引用同步)
|
||||
{
|
||||
static char s_Rename[64] = "";
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputText("##rename", s_Rename, sizeof(s_Rename));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Rename") && s_Rename[0] != '\0' &&
|
||||
states.find(s_Rename) == states.end())
|
||||
{
|
||||
const std::string oldN = selName;
|
||||
const std::string newN = s_Rename;
|
||||
auto node = states.extract(oldN);
|
||||
if (node)
|
||||
{
|
||||
node.key() = newN;
|
||||
states.insert(std::move(node));
|
||||
order[static_cast<size_t>(m_SelState)] = newN;
|
||||
for (auto& t : ctrl->TransitionsMut())
|
||||
{
|
||||
if (t.from == oldN) t.from = newN;
|
||||
if (t.to == oldN) t.to = newN;
|
||||
}
|
||||
for (auto& [n, s] : states)
|
||||
{
|
||||
for (auto& e : s.transitionsOnEnd)
|
||||
{
|
||||
if (e == oldN) e = newN;
|
||||
}
|
||||
}
|
||||
if (ctrl->GetEntryState() == oldN)
|
||||
{
|
||||
ctrl->SetEntry(newN);
|
||||
}
|
||||
s_Rename[0] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 播放分组(speed/loop/播完出口)----
|
||||
if (ImGui::TreeNode("Playback"))
|
||||
{
|
||||
ImGui::DragFloat("speed", &st.speed, 0.05f, 0.1f, 10.0f, "%.2f");
|
||||
ImGui::Checkbox("loop", &st.loop);
|
||||
if (!st.loop)
|
||||
{
|
||||
// 播完出口列表(按序首个可用)+ 下拉追加
|
||||
for (size_t ei = 0; ei < st.transitionsOnEnd.size(); ++ei)
|
||||
{
|
||||
ImGui::PushID(static_cast<int>(ei) + 1000);
|
||||
ImGui::BulletText("%s",
|
||||
st.transitionsOnEnd[ei].c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x"))
|
||||
{
|
||||
st.transitionsOnEnd.erase(
|
||||
st.transitionsOnEnd.begin() +
|
||||
static_cast<long>(ei));
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (!order.empty())
|
||||
{
|
||||
static int s_OnEndSel = 0;
|
||||
s_OnEndSel = std::clamp(
|
||||
s_OnEndSel, 0,
|
||||
static_cast<int>(order.size()) - 1);
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::Combo(
|
||||
"##onend", &s_OnEndSel,
|
||||
[](void* d, int i) -> const char* {
|
||||
return (*static_cast<std::vector<std::string>*>(
|
||||
d))[static_cast<size_t>(i)]
|
||||
.c_str();
|
||||
},
|
||||
&order, static_cast<int>(order.size()));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+ exit"))
|
||||
{
|
||||
st.transitionsOnEnd.push_back(
|
||||
order[static_cast<size_t>(s_OnEndSel)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// ---- 混合树分组 ----
|
||||
if (ImGui::TreeNode("Blend Tree"))
|
||||
{
|
||||
using BTType = Engine::Animation::BlendTree::Type;
|
||||
// 树类型:Single 单 clip / 1D 阈值 / 2D 平面方向
|
||||
int tt = static_cast<int>(st.tree.type);
|
||||
if (ImGui::Combo("type", &tt,
|
||||
"Single\0Directional 1D\0Directional 2D\0"))
|
||||
{
|
||||
st.tree.type = static_cast<BTType>(tt);
|
||||
st.tree.children.clear();
|
||||
if (st.tree.type == BTType::Single)
|
||||
{
|
||||
if (st.tree.clip.empty())
|
||||
{
|
||||
st.tree.clip = FirstClipName(model);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认一个原点 child(clip 取模型首个;无模型 = 手填)
|
||||
st.tree.children.push_back(
|
||||
{FirstClipName(model), glm::vec2{0.0f, 0.0f}});
|
||||
}
|
||||
}
|
||||
// x/z 参数绑定(float 参数下拉)
|
||||
{
|
||||
std::vector<const char*> pnames;
|
||||
for (const auto& p : ctrl->GetParams())
|
||||
{
|
||||
if (p.type ==
|
||||
Engine::Animation::ControllerParam::Type::Float)
|
||||
{
|
||||
pnames.push_back(p.name.c_str());
|
||||
}
|
||||
}
|
||||
if (!pnames.empty())
|
||||
{
|
||||
if (st.tree.type != BTType::Single)
|
||||
{
|
||||
int xi = 0;
|
||||
for (int k = 0; k < static_cast<int>(pnames.size());
|
||||
++k)
|
||||
{
|
||||
if (st.tree.xParam ==
|
||||
pnames[static_cast<size_t>(k)])
|
||||
xi = k;
|
||||
}
|
||||
ImGui::SetNextItemWidth(140.0f);
|
||||
if (ImGui::Combo("x param", &xi, pnames.data(),
|
||||
static_cast<int>(pnames.size())))
|
||||
{
|
||||
st.tree.xParam = pnames[static_cast<size_t>(xi)];
|
||||
}
|
||||
}
|
||||
if (st.tree.type == BTType::Directional2D)
|
||||
{
|
||||
int zi = 0;
|
||||
for (int k = 0; k < static_cast<int>(pnames.size());
|
||||
++k)
|
||||
{
|
||||
if (st.tree.zParam ==
|
||||
pnames[static_cast<size_t>(k)])
|
||||
zi = k;
|
||||
}
|
||||
ImGui::SetNextItemWidth(140.0f);
|
||||
if (ImGui::Combo("z param", &zi, pnames.data(),
|
||||
static_cast<int>(pnames.size())))
|
||||
{
|
||||
st.tree.zParam = pnames[static_cast<size_t>(zi)];
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (st.tree.type != BTType::Single)
|
||||
{
|
||||
ImGui::TextColored(
|
||||
WarnCol(), "need a float param for the blend axis");
|
||||
}
|
||||
}
|
||||
if (st.tree.type == BTType::Single)
|
||||
{
|
||||
ClipRefEdit("clip", st.tree.clip, model);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 原点点缺失警告:v 回到中心时方向层权重全被最小阈值滤
|
||||
// 掉,层空 → 引擎侧 fallback 到首个有效 child 单层(不是
|
||||
// T-pose),但"中心=默认动作"的直觉语义要靠原点点承载
|
||||
if (st.tree.type == BTType::Directional2D)
|
||||
{
|
||||
bool hasOrigin = false;
|
||||
for (const auto& c : st.tree.children)
|
||||
{
|
||||
if (c.pos.x * c.pos.x + c.pos.y * c.pos.y < 1e-8f)
|
||||
hasOrigin = true;
|
||||
}
|
||||
if (!hasOrigin)
|
||||
{
|
||||
ImGui::TextColored(
|
||||
WarnCol(), "! no origin child (at 0,0) - center "
|
||||
"falls back to first child");
|
||||
}
|
||||
}
|
||||
// children 表:clip + 定位 + 删除(2D = (x,y) 平面定位点;
|
||||
// 1D = x 即阈值)。编辑即时生效(System 下帧直读缓存实例)
|
||||
int delChild = -1;
|
||||
for (int ci = 0;
|
||||
ci < static_cast<int>(st.tree.children.size()); ++ci)
|
||||
{
|
||||
auto& c = st.tree.children[static_cast<size_t>(ci)];
|
||||
ImGui::PushID(ci);
|
||||
ClipRefEdit("##clip", c.clip, model);
|
||||
if (st.tree.type == BTType::Directional2D)
|
||||
{
|
||||
ImGui::SetNextItemWidth(70.0f);
|
||||
ImGui::DragFloat("##atx", &c.pos.x, 0.01f, -1.0f,
|
||||
1.0f, "x %.2f");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(70.0f);
|
||||
ImGui::DragFloat("##aty", &c.pos.y, 0.01f, -1.0f,
|
||||
1.0f, "y %.2f");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::DragFloat("##thr", &c.pos.x, 0.05f, -10.0f,
|
||||
10.0f, "thr %.2f");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x")) delChild = ci;
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (delChild >= 0)
|
||||
{
|
||||
st.tree.children.erase(
|
||||
st.tree.children.begin() + delChild);
|
||||
}
|
||||
if (ImGui::SmallButton("+ Add Child"))
|
||||
{
|
||||
st.tree.children.push_back(
|
||||
{FirstClipName(model),
|
||||
st.tree.type == BTType::Directional2D
|
||||
? glm::vec2{1.0f, 0.0f}
|
||||
: glm::vec2{0.0f, 0.0f}});
|
||||
}
|
||||
}
|
||||
ImGui::TreePop(); // Blend Tree
|
||||
}
|
||||
ImGui::Unindent();
|
||||
|
||||
if (delIdx >= 0)
|
||||
{
|
||||
const std::string name = order[static_cast<size_t>(delIdx)];
|
||||
states.erase(name);
|
||||
order.erase(order.begin() + delIdx);
|
||||
auto& trs = ctrl->TransitionsMut();
|
||||
for (int i = static_cast<int>(trs.size()) - 1; i >= 0; --i)
|
||||
{
|
||||
if (trs[static_cast<size_t>(i)].from == name ||
|
||||
trs[static_cast<size_t>(i)].to == name)
|
||||
{
|
||||
trs.erase(trs.begin() + i);
|
||||
}
|
||||
}
|
||||
for (auto& [n, s] : states)
|
||||
{
|
||||
std::erase_if(s.transitionsOnEnd,
|
||||
[&](const std::string& e)
|
||||
{ return e == name; });
|
||||
}
|
||||
if (ctrl->GetEntryState() == name)
|
||||
{
|
||||
ctrl->SetEntry(order.empty() ? "" : order[0]);
|
||||
}
|
||||
m_SelState = 0;
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// ---- Transitions:行编辑(from/to 下拉 + cond 字符串 Enter 编译)----
|
||||
void ControllerEditorPanel::DrawTransitions(
|
||||
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!ImGui::TreeNode("Transitions")) return;
|
||||
const auto& order = ctrl->GetStateOrder();
|
||||
std::vector<const char*> snames;
|
||||
snames.reserve(order.size() + 1);
|
||||
snames.push_back("*"); // AnyState
|
||||
for (const auto& n : order) snames.push_back(n.c_str());
|
||||
auto stateCombo = [&](const char* label, std::string& cur) {
|
||||
int sel = 0;
|
||||
for (int k = 0; k < static_cast<int>(snames.size()); ++k)
|
||||
{
|
||||
if (cur == snames[static_cast<size_t>(k)]) sel = k;
|
||||
}
|
||||
ImGui::SetNextItemWidth(110.0f);
|
||||
if (ImGui::Combo(label, &sel, snames.data(),
|
||||
static_cast<int>(snames.size())))
|
||||
{
|
||||
cur = snames[static_cast<size_t>(sel)];
|
||||
}
|
||||
};
|
||||
|
||||
int delIdx = -1;
|
||||
int ti = 0;
|
||||
for (auto& t : ctrl->TransitionsMut())
|
||||
{
|
||||
ImGui::PushID(ti++);
|
||||
// 行 1:来源 -> 目标(+ 删除)
|
||||
stateCombo("##from", t.from);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted("->");
|
||||
ImGui::SameLine();
|
||||
stateCombo("##to", t.to);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x")) delIdx = ti - 1;
|
||||
// 行 2:条件 + fade + exitTime(缩进——同一过渡的属性组)
|
||||
ImGui::Indent(18.0f);
|
||||
// cond:反推字符串 → Enter(DeactivatedAfterEdit)就地重编译
|
||||
{
|
||||
const std::string s =
|
||||
t.cond.always ? "" :
|
||||
Engine::Animation::AnimationController::CondToString(
|
||||
t.cond);
|
||||
char buf[96];
|
||||
snprintf(buf, sizeof(buf), "%s", s.c_str());
|
||||
ImGui::SetNextItemWidth(170.0f);
|
||||
if (ImGui::InputText("##cond", buf, sizeof(buf),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue) ||
|
||||
ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
t.cond = Engine::Animation::AnimationController::ParseCond(
|
||||
buf);
|
||||
}
|
||||
if (!t.cond.always && !t.cond.valid)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(WarnCol(), "!");
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(60.0f);
|
||||
ImGui::DragFloat("##fade", &t.fade, 0.02f, 0.0f, 5.0f, "%.2f");
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("##exit", &t.hasExitTime);
|
||||
if (ImGui::IsItemHovered()) ImGui::SetTooltip("exit time");
|
||||
ImGui::Unindent(18.0f);
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (delIdx >= 0)
|
||||
{
|
||||
auto& trs = ctrl->TransitionsMut();
|
||||
trs.erase(trs.begin() + delIdx);
|
||||
}
|
||||
if (ImGui::SmallButton("+ Add") && !order.empty())
|
||||
{
|
||||
Engine::Animation::Transition tr;
|
||||
tr.from = order[0];
|
||||
tr.to = order[order.size() > 1 ? 1 : 0];
|
||||
ctrl->TransitionsMut().push_back(tr);
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
|
||||
// ---- Params:参数声明编辑(禁改名——被 tree/cond 引用,改了断链)----
|
||||
void ControllerEditorPanel::DrawParams(
|
||||
const EditorContext& ctx, Engine::Animation::AnimationController* ctrl)
|
||||
{
|
||||
(void)ctx;
|
||||
if (!ImGui::TreeNode("Params")) return;
|
||||
auto& ps = ctrl->ParamsMut();
|
||||
int delIdx = -1;
|
||||
int pi = 0;
|
||||
for (auto& p : ps)
|
||||
{
|
||||
ImGui::PushID(pi++);
|
||||
ImGui::TextUnformatted(p.name.c_str());
|
||||
ImGui::SameLine();
|
||||
int ty = static_cast<int>(p.type);
|
||||
ImGui::SetNextItemWidth(80.0f);
|
||||
if (ImGui::Combo("##ty", &ty, "float\0bool\0trigger\0"))
|
||||
{
|
||||
p.type =
|
||||
static_cast<Engine::Animation::ControllerParam::Type>(ty);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (p.type ==
|
||||
Engine::Animation::ControllerParam::Type::Float)
|
||||
{
|
||||
ImGui::SetNextItemWidth(90.0f);
|
||||
ImGui::DragFloat("##def", &p.defFloat, 0.05f);
|
||||
}
|
||||
else if (p.type ==
|
||||
Engine::Animation::ControllerParam::Type::Bool)
|
||||
{
|
||||
ImGui::Checkbox("##def", &p.defBool);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("x")) delIdx = pi - 1;
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (delIdx >= 0) ps.erase(ps.begin() + delIdx);
|
||||
{
|
||||
static char s_NewName[64] = "";
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
ImGui::InputText("##new_param", s_NewName, sizeof(s_NewName));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("+ Add") && s_NewName[0] != '\0')
|
||||
{
|
||||
bool dup = false;
|
||||
for (const auto& p : ps)
|
||||
{
|
||||
if (p.name == s_NewName) dup = true;
|
||||
}
|
||||
if (!dup)
|
||||
{
|
||||
Engine::Animation::ControllerParam d;
|
||||
d.name = s_NewName;
|
||||
ps.push_back(d);
|
||||
s_NewName[0] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// ControllerEditorPanel:动画控制器编辑器窗口(Unity Animator 状态机
|
||||
// 面板范式,交互对齐 MaterialEditorPanel 的"指派/编辑分离")。
|
||||
//
|
||||
// Details 的 Controller 区只做指派(槽/Create/双击打开本面板);
|
||||
// 结构编辑全在这里:
|
||||
// - Preview:方向盘(2D 混合树可视化驱动)+ 权重实时读出,拖动
|
||||
// SetFloat 到当前选中实体(Controller 模式)实时预览
|
||||
// - States:状态列表(增删/重命名/entry),选中状态详情 = 播放
|
||||
// 参数 + 混合树类型 + children 逐行编辑(clip 指定/定位/删除)
|
||||
// - Transitions / Params:列表编辑,cond 字符串 Enter 就地编译
|
||||
//
|
||||
// 编辑直接写缓存资产实例(System 下一帧生效,无需保存);Save 落盘
|
||||
// + ReloadController 重编译(实例换新,本面板每帧重解析不持指针)。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_CONTROLLEREDITORPANEL_H
|
||||
#define HELLOVULKAN_CONTROLLEREDITORPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// .animctrl 拖拽 payload(AssetBrowser → Details Controller 槽指派)
|
||||
inline constexpr char kControllerDragPayload[] = "CTRL_PATH";
|
||||
|
||||
class ControllerEditorPanel {
|
||||
public:
|
||||
// 打开 .animctrl 资产(Details 槽位 Edit 按钮 / Content Browser
|
||||
// 双击入口)
|
||||
void Open(const std::string& path)
|
||||
{
|
||||
m_Path = path;
|
||||
m_Open = true;
|
||||
m_SelState = 0;
|
||||
}
|
||||
bool IsOpen() const { return m_Open; }
|
||||
void Draw(const EditorContext& ctx);
|
||||
|
||||
private:
|
||||
void DrawPreview(const EditorContext& ctx);
|
||||
void DrawStates(const EditorContext& ctx,
|
||||
Engine::Animation::AnimationController* ctrl);
|
||||
void DrawTransitions(const EditorContext& ctx,
|
||||
Engine::Animation::AnimationController* ctrl);
|
||||
void DrawParams(const EditorContext& ctx,
|
||||
Engine::Animation::AnimationController* ctrl);
|
||||
|
||||
bool m_Open = false;
|
||||
std::string m_Path;
|
||||
int m_SelState = 0; // 选中状态(StateOrder 下标)
|
||||
std::vector<char> m_CondBuf; // cond 输入缓冲(按行复用)
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_CONTROLLEREDITORPANEL_H
|
||||
@@ -15,6 +15,8 @@
|
||||
#include "AssetBrowserPanel.h"
|
||||
#include "HierarchyPanel.h"
|
||||
#include "InspectorPanel.h"
|
||||
#include "MaterialEditorPanel.h"
|
||||
#include "ControllerEditorPanel.h"
|
||||
#include "ViewportPanel.h"
|
||||
#include "StatsPanel.h"
|
||||
|
||||
@@ -47,6 +49,11 @@ void Editor::Init(const EditorContext& ctx)
|
||||
m_Viewport = std::make_unique<ViewportPanel>();
|
||||
m_Stats = std::make_unique<StatsPanel>();
|
||||
m_AssetBrowser = std::make_unique<AssetBrowserPanel>();
|
||||
m_MaterialEditor = std::make_unique<MaterialEditorPanel>();
|
||||
m_Ctx.materialEditor = m_MaterialEditor.get();
|
||||
m_ControllerEditor = std::make_unique<ControllerEditorPanel>();
|
||||
m_Ctx.controllerEditor = m_ControllerEditor.get();
|
||||
m_Ctx.selected = &m_Selected;
|
||||
|
||||
// 旧布局一次性迁移:面板改名(Hierarchy→Outliner / Inspector→Details /
|
||||
// Asset Browser→Content Browser)= docking 身份全变,旧 imgui.ini 对
|
||||
@@ -192,6 +199,8 @@ void Editor::Draw()
|
||||
}
|
||||
if (m_ShowStats) m_Stats->Draw(m_Ctx);
|
||||
if (m_ShowAssetBrowser) m_AssetBrowser->Draw(m_Ctx);
|
||||
if (m_ShowMaterialEditor) m_MaterialEditor->Draw(m_Ctx);
|
||||
m_ControllerEditor->Draw(m_Ctx); // 面板内部 m_Open 门控
|
||||
|
||||
DrawQuitModal();
|
||||
DrawSceneModals();
|
||||
@@ -269,6 +278,7 @@ void Editor::DrawMenuBar()
|
||||
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
|
||||
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
|
||||
ImGui::MenuItem("Content Browser", nullptr, &m_ShowAssetBrowser);
|
||||
ImGui::MenuItem("Material Editor", nullptr, &m_ShowMaterialEditor);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
@@ -399,6 +409,8 @@ void Editor::DrawDockSpace()
|
||||
ImGui::DockBuilderDockWindow("Stats", dockCenter); // Viewport 旁 tab
|
||||
ImGui::DockBuilderDockWindow("Content Browser", dockBottom);
|
||||
ImGui::DockBuilderDockWindow("Details", dockRight);
|
||||
ImGui::DockBuilderDockWindow("Material Editor", dockRight); // Details 旁 tab
|
||||
ImGui::DockBuilderDockWindow("Controller Editor", dockRight); // Details 旁 tab
|
||||
|
||||
ImGui::DockBuilderFinish(dockspaceId);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ class InspectorPanel;
|
||||
class ViewportPanel;
|
||||
class StatsPanel;
|
||||
class AssetBrowserPanel;
|
||||
class MaterialEditorPanel;
|
||||
class ControllerEditorPanel;
|
||||
|
||||
class Editor {
|
||||
public:
|
||||
@@ -108,6 +110,8 @@ private:
|
||||
std::unique_ptr<ViewportPanel> m_Viewport;
|
||||
std::unique_ptr<StatsPanel> m_Stats;
|
||||
std::unique_ptr<AssetBrowserPanel> m_AssetBrowser;
|
||||
std::unique_ptr<MaterialEditorPanel> m_MaterialEditor;
|
||||
std::unique_ptr<ControllerEditorPanel> m_ControllerEditor;
|
||||
|
||||
entt::entity m_Selected{entt::null};
|
||||
|
||||
@@ -117,6 +121,7 @@ private:
|
||||
bool m_ShowViewport = true;
|
||||
bool m_ShowStats = true;
|
||||
bool m_ShowAssetBrowser = true;
|
||||
bool m_ShowMaterialEditor = true;
|
||||
|
||||
// 默认布局只建一次(imgui.ini 已有布局则跳过)
|
||||
bool m_LayoutBuilt = false;
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
#include <Engine/Renderer/EditorCamera.h>
|
||||
#include <Engine/Renderer/SceneTypes.h> // kDisplayViewport / kDisplayDirect
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
class MaterialEditorPanel; // 面板间联动通道(Details 双击槽位 / Content
|
||||
// Browser 双击 .material → 打开材质编辑器)
|
||||
class ControllerEditorPanel; // Details Controller 槽 Edit → 打开控制器编辑器
|
||||
|
||||
// 沙盒特有状态(面板可选读取,空指针 = 该面板区块不显示)
|
||||
struct EditorContext {
|
||||
// 引擎门面
|
||||
@@ -25,6 +31,12 @@ struct EditorContext {
|
||||
// 编辑器相机(Scene view 专用,归 Renderer 持有——与场景相机实体
|
||||
// 无关;面板经此做 F 焦点/Gizmo 视图/对齐跳转)
|
||||
Engine::EditorCamera *editorCamera = nullptr;
|
||||
// 材质编辑器面板(Editor 持有;指派/编辑分离的联动入口)
|
||||
MaterialEditorPanel *materialEditor = nullptr;
|
||||
// 控制器编辑器面板(Editor 持有;指派/编辑分离的联动入口)
|
||||
ControllerEditorPanel *controllerEditor = nullptr;
|
||||
// 当前选中实体(Controller 编辑器 Preview 的驱动目标)
|
||||
entt::entity *selected = nullptr;
|
||||
|
||||
// PIE 运行态(Play-In-Editor):Editor 持有,只读指针。
|
||||
// 面板门控用——运行态禁 Gizmo/编辑器热键,渲染源已是 primary 相机
|
||||
|
||||
+783
-23
@@ -6,22 +6,38 @@
|
||||
//
|
||||
|
||||
#include "InspectorPanel.h"
|
||||
#include "MaterialAssetUtil.h" // .material 枚举 + Create Override(与材质编辑器共享)
|
||||
#include "MaterialEditorPanel.h" // 指派/编辑分离:双击槽位开独立面板
|
||||
|
||||
#include <functional> // ComponentHeader 组件专属菜单项
|
||||
#include "Engine/ECS/Components.h"
|
||||
#include "Engine/Core/Types.h"
|
||||
#include "Engine/Animation/AnimationController.h" // Controller 模式 UI(参数/状态枚举)
|
||||
#include "Engine/Renderer/Model.h"
|
||||
#include "Engine/Renderer/MaterialAsset.h" // 材质覆盖槽(.material 资产)
|
||||
#include "Engine/Physics/PhysicsWorld.h" // Apply Impulse 调试(GetPhysics 门面)
|
||||
#include "Engine/Scripting/Script.h" // 脚本下拉(ScriptRegistry::Names)
|
||||
#include "Engine/Core/Log.h" // Create Override 失败告警
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include "ControllerEditorPanel.h"
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <type_traits>
|
||||
|
||||
namespace
|
||||
{
|
||||
// 材质槽循环索引用(u32 定义于 namespace Engine,App 层显式引入)
|
||||
using Engine::u32;
|
||||
|
||||
// 递归收集 assets/ 下模型容器文件(模型槽与动画 attach 槽共用;
|
||||
// 无法从扩展名区分"模型 fbx"与"纯动画 fbx",一并列出)。
|
||||
// 会话级缓存:目录条目几十量级,避免每帧递归扫描
|
||||
@@ -58,6 +74,42 @@ namespace
|
||||
return s_Files;
|
||||
}
|
||||
|
||||
// 递归收集 assets/ 下动画控制器资产(.animctrl,Controller 槽用;
|
||||
// 会话级缓存,Create 新文件后经 InvalidateControllerFiles 失效)
|
||||
static std::vector<std::string> s_CtrlFiles;
|
||||
static bool s_CtrlFilesCached = false;
|
||||
void InvalidateControllerFiles() { s_CtrlFilesCached = false; }
|
||||
const std::vector<std::string>& AssetControllerFiles()
|
||||
{
|
||||
if (!s_CtrlFilesCached)
|
||||
{
|
||||
s_CtrlFilesCached = true;
|
||||
s_CtrlFiles.clear();
|
||||
namespace fs = std::filesystem;
|
||||
if (fs::exists("assets"))
|
||||
{
|
||||
for (const auto& entry :
|
||||
fs::recursive_directory_iterator("assets"))
|
||||
{
|
||||
if (!entry.is_regular_file())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::string ext = entry.path().extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c)
|
||||
{ return std::tolower(c); });
|
||||
if (ext == ".animctrl")
|
||||
{
|
||||
s_CtrlFiles.push_back(entry.path().generic_string());
|
||||
}
|
||||
}
|
||||
std::sort(s_CtrlFiles.begin(), s_CtrlFiles.end());
|
||||
}
|
||||
}
|
||||
return s_CtrlFiles;
|
||||
}
|
||||
|
||||
// 递归收集 assets/ 下环境贴图(.hdr,Skybox 资产槽用;会话级缓存同上)
|
||||
const std::vector<std::string>& AssetEnvFiles()
|
||||
{
|
||||
@@ -104,6 +156,217 @@ namespace
|
||||
return false;
|
||||
}
|
||||
|
||||
// MeshRenderer 材质槽列表(UE Material Slots 语义):内建材质只读,
|
||||
// 覆盖槽 = .material 资产引用,可换绑/还原。参数/纹理"编辑"在独立
|
||||
// Material Editor 面板(双击槽位球打开)——指派与编辑分离,
|
||||
// 本行只做指派(UE Details 材质槽交互)
|
||||
void DrawMaterialSlots(const EditorContext& ctx,
|
||||
const Engine::Model& model,
|
||||
Engine::ECS::MeshRendererComponent& mr,
|
||||
entt::entity owner,
|
||||
Engine::Assets::AssetManager& assets,
|
||||
Engine::Renderer& renderer)
|
||||
{
|
||||
const auto& meshes = model.GetMeshes();
|
||||
const auto& infos = model.GetMeshMaterialInfos();
|
||||
ImGui::SeparatorText("Material Slots");
|
||||
constexpr float kPreviewSize = 48.0f;
|
||||
|
||||
for (u32 i = 0; i < meshes.size(); ++i)
|
||||
{
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
|
||||
const std::string meshName =
|
||||
(i < infos.size() && !infos[i].name.empty())
|
||||
? infos[i].name
|
||||
: "Slot " + std::to_string(i);
|
||||
|
||||
// ---- 材质球预览(行左列 48²,tile = 池分配)----
|
||||
// 材质来源与渲染收集期寻址一致:覆盖资产 > 内建。
|
||||
// UE FAssetThumbnailPool 语义:只对滚进视口的行发幂等请求
|
||||
//(IsRectVisible 探测,132 槽模型只处理可见行),池按
|
||||
// serial+参数版本判缓存——渲染一次跨帧持久,EditParams bump
|
||||
// 版本后下帧重渲该 tile;-1 = 首渲未完成/池满(占位排队)
|
||||
auto it = mr.materialOverrides.find(i);
|
||||
Engine::Material* slotMat = nullptr;
|
||||
const char* slotSource = "built-in";
|
||||
if (it != mr.materialOverrides.end())
|
||||
{
|
||||
if (Engine::MaterialAsset* asset =
|
||||
assets.FindMaterialAsset(it->second))
|
||||
{
|
||||
slotMat = asset->GetMaterial();
|
||||
slotSource = it->second.c_str();
|
||||
}
|
||||
}
|
||||
if (!slotMat)
|
||||
{
|
||||
slotMat = meshes[i]->GetMaterial();
|
||||
}
|
||||
|
||||
// 可见性探测先行:请求只来自可见行(池 LRU 换页的前提)
|
||||
const bool rowVisible =
|
||||
ImGui::IsRectVisible(ImVec2(kPreviewSize, kPreviewSize));
|
||||
Engine::i32 tile = -1;
|
||||
if (slotMat && rowVisible)
|
||||
{
|
||||
tile = renderer.RequestMaterialPreview(slotMat);
|
||||
}
|
||||
|
||||
// ---- 球 + 拖放目标(Content Browser 拖 .material 指派)----
|
||||
void* atlasTex = renderer.GetMaterialPreviewTexture();
|
||||
if (atlasTex && tile >= 0)
|
||||
{
|
||||
const float uvStep =
|
||||
1.0f / float(Engine::Passes::kPreviewTilesPerSide);
|
||||
const float u0 =
|
||||
float(tile % Engine::Passes::kPreviewTilesPerSide) * uvStep;
|
||||
const float v0 =
|
||||
float(tile / Engine::Passes::kPreviewTilesPerSide) * uvStep;
|
||||
ImGui::Image(atlasTex, ImVec2(kPreviewSize, kPreviewSize),
|
||||
ImVec2(u0, v0),
|
||||
ImVec2(u0 + uvStep, v0 + uvStep));
|
||||
}
|
||||
else
|
||||
{
|
||||
// atlas 未就绪(首帧前)/ tile 首渲排队中:占位保持行高
|
||||
ImGui::Dummy(ImVec2(kPreviewSize, kPreviewSize));
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::SetTooltip("%s\nSource: %s\nDouble-click to edit",
|
||||
meshName.c_str(), slotSource);
|
||||
}
|
||||
|
||||
// ---- 双击球 = 打开 Material Editor(指派/编辑分离)----
|
||||
if (ctx.materialEditor && ImGui::IsItemHovered() &&
|
||||
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
if (it != mr.materialOverrides.end())
|
||||
{
|
||||
ctx.materialEditor->OpenAsset(it->second);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.materialEditor->OpenBuiltin(owner, i, meshName);
|
||||
}
|
||||
}
|
||||
if (ImGui::BeginDragDropTarget())
|
||||
{
|
||||
if (const ImGuiPayload* pl = ImGui::AcceptDragDropPayload(
|
||||
kMaterialDragPayload))
|
||||
{
|
||||
const std::string dropped(
|
||||
static_cast<const char*>(pl->Data));
|
||||
if (assets.LoadMaterialAsset(dropped).Valid())
|
||||
{
|
||||
mr.materialOverrides[i] = dropped;
|
||||
}
|
||||
it = mr.materialOverrides.find(i);
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
ImGui::TextUnformatted(meshName.c_str());
|
||||
|
||||
if (it == mr.materialOverrides.end())
|
||||
{
|
||||
// 内建只读:一键生成覆盖资产(成功即自动进编辑器)
|
||||
if (i < infos.size() &&
|
||||
ImGui::SmallButton("Create Override"))
|
||||
{
|
||||
const std::string rel =
|
||||
EditorMaterialUtil::CreateMaterialOverride(
|
||||
assets.Models().Path(mr.model), infos[i], i, mr,
|
||||
assets);
|
||||
if (!rel.empty() && ctx.materialEditor)
|
||||
{
|
||||
ctx.materialEditor->OpenAsset(rel);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string& cur = it->second;
|
||||
|
||||
// 资产文件下拉(当前路径不在枚举里也插入显示,防错位)
|
||||
const auto& files =
|
||||
EditorMaterialUtil::AssetMaterialFiles();
|
||||
std::vector<const char*> opts;
|
||||
opts.push_back("<none>");
|
||||
bool curListed = false;
|
||||
for (const auto& f : files)
|
||||
{
|
||||
opts.push_back(f.c_str());
|
||||
curListed |= (f == cur);
|
||||
}
|
||||
if (!curListed)
|
||||
{
|
||||
opts.insert(opts.begin() + 1, cur.c_str());
|
||||
}
|
||||
int sel = -1;
|
||||
for (int k = 0; k < static_cast<int>(opts.size()); ++k)
|
||||
{
|
||||
if (std::strcmp(opts[static_cast<size_t>(k)],
|
||||
cur.c_str()) == 0)
|
||||
{
|
||||
sel = k;
|
||||
}
|
||||
}
|
||||
|
||||
const float revertW = ImGui::CalcTextSize("Revert").x +
|
||||
ImGui::GetStyle().FramePadding.x * 2.0f;
|
||||
ImGui::SetNextItemWidth(
|
||||
ImGui::GetContentRegionAvail().x - revertW -
|
||||
ImGui::GetStyle().ItemSpacing.x);
|
||||
if (ImGui::Combo("##mat", &sel, opts.data(),
|
||||
static_cast<int>(opts.size())))
|
||||
{
|
||||
if (sel <= 0)
|
||||
{
|
||||
mr.materialOverrides.erase(i);
|
||||
}
|
||||
else
|
||||
{
|
||||
const char* chosen = opts[static_cast<size_t>(sel)];
|
||||
if (assets.LoadMaterialAsset(chosen).Valid())
|
||||
{
|
||||
mr.materialOverrides[i] = chosen;
|
||||
}
|
||||
}
|
||||
it = mr.materialOverrides.find(i);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Revert"))
|
||||
{
|
||||
mr.materialOverrides.erase(i);
|
||||
it = mr.materialOverrides.end();
|
||||
}
|
||||
|
||||
// 覆盖资产未加载(罕见):就地补救(编辑器打开会自动 Load)
|
||||
if (it != mr.materialOverrides.end() &&
|
||||
assets.FindMaterialAsset(it->second) == nullptr)
|
||||
{
|
||||
ImGui::TextDisabled(" override not loaded");
|
||||
if (ImGui::SmallButton("Retry Load"))
|
||||
{
|
||||
assets.LoadMaterialAsset(it->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndGroup();
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
ImGui::TextDisabled(
|
||||
"double-click a sphere to edit; edits on shared .material "
|
||||
"apply to all entities referencing it");
|
||||
}
|
||||
|
||||
// UE 风变换行:label + 三轴色 DragFloat(轴色对齐 Gizmo:X 红 /
|
||||
// Y 绿 / Z 蓝)。拖拽粗调;Ctrl+Click 进输入框键入精确值。
|
||||
// 任一轴变化返回 true。fmt = 显示格式(旋转行传 "%.1f" 对齐
|
||||
@@ -146,11 +409,13 @@ namespace
|
||||
return changed;
|
||||
}
|
||||
|
||||
// 组件区块头(Unity 语义):折叠标题 + 右缘"启用"勾选与"删除"按钮。
|
||||
// active 传 nullptr = 无启停(结构性组件);removable = 显示删除钮,
|
||||
// 点击时置 *removeFlag,调用方在区块末尾统一移除(避免边画边删)
|
||||
// 组件区块头(Unity 语义):折叠标题 + 右缘"启用"勾选与"..."菜单。
|
||||
// active 传 nullptr = 无启停(结构性组件);removable = 显示"...",
|
||||
// 菜单内含 Remove;extraMenu(可空)= 组件专属菜单项(如 Camera 的
|
||||
// 视角联动),画在 Remove 之前
|
||||
bool ComponentHeader(const char* label, bool* active, bool removable,
|
||||
bool* removeFlag)
|
||||
bool* removeFlag,
|
||||
const std::function<void()>& extraMenu = {})
|
||||
{
|
||||
ImGui::PushID(label);
|
||||
bool open;
|
||||
@@ -163,12 +428,23 @@ namespace
|
||||
float x = ImGui::GetWindowWidth() - 32.0f;
|
||||
if (removable)
|
||||
{
|
||||
// "..." 菜单(UE Details 组件头语义):移除等组件操作收进
|
||||
// 下拉,头部不再放裸删除钮(防误触)
|
||||
ImGui::SameLine(x);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 1.0f);
|
||||
if (ImGui::Button("-", ImVec2(24.0f, rowH - 4.0f)))
|
||||
if (ImGui::Button("...", ImVec2(24.0f, rowH - 4.0f)))
|
||||
{
|
||||
ImGui::OpenPopup("comp_menu");
|
||||
}
|
||||
if (ImGui::BeginPopup("comp_menu"))
|
||||
{
|
||||
if (extraMenu) extraMenu();
|
||||
if (ImGui::MenuItem("Remove Component"))
|
||||
{
|
||||
*removeFlag = true;
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
x -= 30.0f;
|
||||
}
|
||||
ImGui::SameLine(x);
|
||||
@@ -366,6 +642,9 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
mr->model = (sel <= 0)
|
||||
? Engine::ModelRef{}
|
||||
: ctx.assets->LoadModel(s_ModelFiles[static_cast<size_t>(sel) - 1]);
|
||||
// 模型换绑后 mesh 下标语义失效:覆盖表显式清空
|
||||
//(残留会错挂到新模型的槽位上)
|
||||
mr->materialOverrides.clear();
|
||||
}
|
||||
ImGui::Spacing();
|
||||
}
|
||||
@@ -404,6 +683,16 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
{
|
||||
ImGui::TextDisabled("no model assigned");
|
||||
}
|
||||
// ---- 材质槽(UE Material Slots:内建只读 + .material 覆盖)----
|
||||
if (mr->model.Valid())
|
||||
{
|
||||
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
|
||||
model != nullptr && !model->GetMeshes().empty())
|
||||
{
|
||||
DrawMaterialSlots(ctx, *model, *mr, selected, *ctx.assets,
|
||||
*ctx.renderer);
|
||||
}
|
||||
}
|
||||
if (!mr->active)
|
||||
{
|
||||
ImGui::EndDisabled();
|
||||
@@ -529,7 +818,34 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
|
||||
{
|
||||
bool removeCam = false;
|
||||
if (ComponentHeader("Camera", &cam->active, true, &removeCam))
|
||||
// "..." 菜单组件专属项(双向同步,均只取位置+朝向,缩放不动):
|
||||
// ① 编辑器相机 → 飞到本实体机位(编辑器视角立即从该相机处看)
|
||||
// ② 本实体 → 摆到编辑器当前机位(摆实体相机,省手动对位)
|
||||
// SetWorldPose 沿父链反解 local(层级实体不破坏父关系)
|
||||
auto camMenu = [&]()
|
||||
{
|
||||
if (ImGui::MenuItem("Move Editor Camera Here") && ctx.editorCamera)
|
||||
{
|
||||
if (auto* tr = reg.try_get<Engine::ECS::TransformComponent>(
|
||||
selected))
|
||||
{
|
||||
ctx.editorCamera->AlignTo(*tr);
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem("Move to Editor Camera Position") &&
|
||||
ctx.editorCamera)
|
||||
{
|
||||
const auto& ed = ctx.editorCamera->GetTransform();
|
||||
if (auto* tr = reg.try_get<Engine::ECS::TransformComponent>(
|
||||
selected))
|
||||
{
|
||||
ctx.scene->SetWorldPose(selected, ed.GetWorldPosition(),
|
||||
ed.GetWorldRotationQuat(),
|
||||
tr->GetWorldScale());
|
||||
}
|
||||
}
|
||||
};
|
||||
if (ComponentHeader("Camera", &cam->active, true, &removeCam, camMenu))
|
||||
{
|
||||
if (!cam->active)
|
||||
{
|
||||
@@ -549,17 +865,46 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
});
|
||||
}
|
||||
|
||||
// 对齐跳转:把本实体位姿复制给编辑器相机,Scene view 立即
|
||||
// 从该位置观看(只取位姿,不接管渲染——渲染源分派在
|
||||
// SceneRenderer,编辑器形态恒用编辑器相机)
|
||||
if (ImGui::Button("Align Viewport to Camera") && ctx.editorCamera)
|
||||
// ---- 投影参数(UE/Unity Camera 语义):模式二选一,fov 与
|
||||
// ortho size 按模式互斥显示;near/far 成对写回(SetClip
|
||||
// 内部保证 far > near)----
|
||||
const char* projModes[] = {"Perspective", "Orthographic"};
|
||||
int pm = static_cast<int>(cam->camera.GetProjectionMode());
|
||||
if (ImGui::Combo("Projection", &pm, projModes, 2))
|
||||
{
|
||||
if (auto* tr = reg.try_get<Engine::ECS::TransformComponent>(
|
||||
selected))
|
||||
cam->camera.SetProjectionMode(
|
||||
static_cast<Engine::Camera::ProjectionMode>(pm));
|
||||
}
|
||||
if (cam->camera.GetProjectionMode() ==
|
||||
Engine::Camera::ProjectionMode::Perspective)
|
||||
{
|
||||
ctx.editorCamera->AlignTo(*tr);
|
||||
float fov = cam->camera.GetFovDeg();
|
||||
if (ImGui::DragFloat("FOV (deg)", &fov, 0.1f, 1.0f, 179.0f,
|
||||
"%.1f"))
|
||||
{
|
||||
cam->camera.SetFovDeg(fov);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float orthoSize = cam->camera.GetOrthoSize();
|
||||
if (ImGui::DragFloat("Ortho Size", &orthoSize, 0.1f, 0.01f,
|
||||
10000.0f, "%.2f"))
|
||||
{
|
||||
cam->camera.SetOrthoSize(orthoSize);
|
||||
}
|
||||
}
|
||||
float nearZ = cam->camera.GetNearZ();
|
||||
float farZ = cam->camera.GetFarZ();
|
||||
const bool nearChanged = ImGui::DragFloat(
|
||||
"Near Z", &nearZ, 0.01f, 0.001f, 1000.0f, "%.3f");
|
||||
const bool farChanged = ImGui::DragFloat(
|
||||
"Far Z", &farZ, 1.0f, 0.1f, 100000.0f, "%.1f");
|
||||
if (nearChanged || farChanged)
|
||||
{
|
||||
cam->camera.SetClip(nearZ, farZ);
|
||||
}
|
||||
|
||||
if (!cam->active)
|
||||
{
|
||||
ImGui::EndDisabled();
|
||||
@@ -951,6 +1296,14 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
}
|
||||
|
||||
// ---- Animation ----
|
||||
// attach 遥测(会话级 static:最近一次并入的骨骼匹配结果,
|
||||
// 面板内显示"matched/skipped")
|
||||
static std::string s_LastAttachFile;
|
||||
static unsigned s_LastAttachMatched = 0, s_LastAttachSkipped = 0;
|
||||
static unsigned s_LastAttachRetargeted = 0;
|
||||
static std::vector<Engine::Animation::RetargetRow> s_LastAttachRows;
|
||||
// 时间拖动条的定格记忆:拖住瞬间是否在播(松手恢复用)
|
||||
static bool s_ScrubWasPlaying = false;
|
||||
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
|
||||
{
|
||||
bool removeAnim = false;
|
||||
@@ -966,6 +1319,32 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
(mr && mr->model.Valid()) ? ctx.assets->GetModel(mr->model)
|
||||
: nullptr;
|
||||
|
||||
// ---- 骨骼可视化开关(Endfield-Poser 三件套)----全局编辑器
|
||||
// 设置(经 Renderer 门面),非实体数据、不落场景序列化。
|
||||
// 开 = 3D 骨架线 + 视口关节圆点;All Nodes = 显示全部节点
|
||||
// 层级(普通节点暗色),关 = 仅骨骼链;圆点大小按手感调
|
||||
{
|
||||
bool show = ctx.renderer->GetBoneWiresVisible();
|
||||
if (ImGui::Checkbox("Show Bones", &show))
|
||||
{
|
||||
ctx.renderer->SetBoneWiresVisible(show);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
bool allNodes = ctx.renderer->GetBoneAllNodes();
|
||||
if (ImGui::Checkbox("All Nodes", &allNodes))
|
||||
{
|
||||
ctx.renderer->SetBoneAllNodes(allNodes);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
float px = ctx.renderer->GetBoneJointPx();
|
||||
ImGui::SetNextItemWidth(130.0f);
|
||||
if (ImGui::SliderFloat("##bone_joint_px", &px, 2.0f, 14.0f,
|
||||
"joint %.0fpx"))
|
||||
{
|
||||
ctx.renderer->SetBoneJointPx(px);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 外部动画并入(自愈重放 + attach 槽)----
|
||||
// 重放:attachedAnimPaths 里未并入的源(重启后 Model 缓存是
|
||||
// 空白)幂等重 attach;解析失败的路径会话内不再重试
|
||||
@@ -990,6 +1369,298 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Mode:Direct(单 clip 直控)/ Controller(.animctrl
|
||||
// 状态机)----Unity Animator 语义:Controller 下播放由
|
||||
// 状态机调度(Direct 字段不参与求值),切回 Direct 配置
|
||||
// 不丢
|
||||
{
|
||||
ImGui::SeparatorText("Mode");
|
||||
using AnimMode = Engine::ECS::AnimationComponent::AnimMode;
|
||||
int modeSel =
|
||||
anim->mode == AnimMode::Controller ? 1 : 0;
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (ImGui::Combo("##anim_mode", &modeSel,
|
||||
"Direct\0Controller\0"))
|
||||
{
|
||||
anim->mode =
|
||||
modeSel == 1 ? AnimMode::Controller : AnimMode::Direct;
|
||||
}
|
||||
if (anim->mode == AnimMode::Controller)
|
||||
{
|
||||
// ---- Controller 资产槽(指派即预载)----
|
||||
const auto& cfiles = AssetControllerFiles();
|
||||
std::vector<const char*> copts;
|
||||
copts.push_back("<none>");
|
||||
for (const auto& f : cfiles)
|
||||
{
|
||||
copts.push_back(f.c_str());
|
||||
}
|
||||
int csel = 0;
|
||||
for (size_t k = 0; k < cfiles.size(); ++k)
|
||||
{
|
||||
if (cfiles[k] == anim->controllerPath)
|
||||
{
|
||||
csel = static_cast<int>(k) + 1;
|
||||
}
|
||||
}
|
||||
ImGui::SetNextItemWidth(-1.0f);
|
||||
if (ImGui::Combo("##anim_ctrl_slot", &csel, copts.data(),
|
||||
static_cast<int>(copts.size())))
|
||||
{
|
||||
anim->currentState.clear(); // 换绑 → 首帧回 entry
|
||||
if (csel == 0)
|
||||
{
|
||||
anim->controllerPath.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string& path =
|
||||
cfiles[static_cast<size_t>(csel) - 1];
|
||||
anim->controllerPath = path;
|
||||
ctx.assets->LoadController(path);
|
||||
// 参数默认值初始化(只补缺键——调好的值不覆盖)
|
||||
if (const auto* ctrl =
|
||||
ctx.assets->FindController(path))
|
||||
{
|
||||
for (const auto& p : ctrl->GetParams())
|
||||
{
|
||||
if (p.type ==
|
||||
Engine::Animation::ControllerParam::
|
||||
Type::Bool)
|
||||
{
|
||||
anim->boolParams.emplace(p.name,
|
||||
p.defBool);
|
||||
}
|
||||
else if (p.type ==
|
||||
Engine::Animation::
|
||||
ControllerParam::Type::Float)
|
||||
{
|
||||
anim->floatParams.emplace(p.name,
|
||||
p.defFloat);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 双击槽 = 打开 Controller Editor(Unity 双击
|
||||
// Controller 资产打开 Animator 窗口语义)
|
||||
if (csel > 0 && ImGui::IsItemHovered() &&
|
||||
ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) &&
|
||||
ctx.controllerEditor != nullptr)
|
||||
{
|
||||
ctx.controllerEditor->Open(anim->controllerPath);
|
||||
}
|
||||
// 拖放指派:AssetBrowser 的 .animctrl 拖进来
|
||||
//(对齐材质槽交互)
|
||||
if (ImGui::BeginDragDropTarget())
|
||||
{
|
||||
if (const ImGuiPayload* pl =
|
||||
ImGui::AcceptDragDropPayload(
|
||||
kControllerDragPayload))
|
||||
{
|
||||
const std::string path(
|
||||
static_cast<const char*>(pl->Data));
|
||||
anim->currentState.clear(); // 首帧回 entry
|
||||
anim->controllerPath = path;
|
||||
ctx.assets->LoadController(path);
|
||||
if (const auto* c =
|
||||
ctx.assets->FindController(path))
|
||||
{
|
||||
for (const auto& p : c->GetParams())
|
||||
{
|
||||
if (p.type ==
|
||||
Engine::Animation::
|
||||
ControllerParam::Type::Float)
|
||||
{
|
||||
anim->floatParams.emplace(
|
||||
p.name, p.defFloat);
|
||||
}
|
||||
else if (p.type ==
|
||||
Engine::Animation::
|
||||
ControllerParam::Type::
|
||||
Bool)
|
||||
{
|
||||
anim->boolParams.emplace(p.name,
|
||||
p.defBool);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
const auto* ctrl =
|
||||
anim->controllerPath.empty()
|
||||
? nullptr
|
||||
: ctx.assets->FindController(
|
||||
anim->controllerPath);
|
||||
// ---- Create / Reload 工具行:.animctrl = 纯文本
|
||||
// YAML 资产——Create 从当前模型 clip 自动生成
|
||||
// 可跑模板;外部编辑器改完点 Reload 热重载 ----
|
||||
{
|
||||
static char s_NewName[96] = "my_controller";
|
||||
static bool s_CreateOk = true;
|
||||
ImGui::SetNextItemWidth(150.0f);
|
||||
ImGui::InputText("##ctrl_new_name", s_NewName,
|
||||
sizeof(s_NewName));
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Create") && s_NewName[0] != '\0')
|
||||
{
|
||||
// 模板 = Idle(单 clip)+ Locomotion(2D 方向
|
||||
// 树:原点 + 4 方向 walk 占位)+ Speed 过渡
|
||||
// ——生成即有方向盘,打开 Controller Editor
|
||||
// 再逐点指定 clip;无模型时用占位名(clip 查
|
||||
// 不到 = rest 兜底)
|
||||
std::vector<std::string> clips;
|
||||
if (model != nullptr)
|
||||
{
|
||||
for (const auto& c : model->GetClips())
|
||||
{
|
||||
clips.push_back(c.name);
|
||||
}
|
||||
}
|
||||
if (clips.empty())
|
||||
{
|
||||
clips.push_back("clip_name");
|
||||
}
|
||||
const auto clipAt = [&](size_t i) {
|
||||
return clips.empty()
|
||||
? "clip_name"
|
||||
: clips[i % clips.size()];
|
||||
};
|
||||
std::string yaml =
|
||||
"# 动画控制器:状态/参数/过渡全由本文件声明,"
|
||||
"引擎零硬编码。\n"
|
||||
"# 纯文本 YAML —— 也可在 Controller Editor "
|
||||
"面板可视化编辑后 Save。\n"
|
||||
"# 条件语法:\"参数 op 值\"(> < >= <= == "
|
||||
"!=;值 = 数字或 true/false)\n"
|
||||
"Version: 1\n"
|
||||
"Params:\n"
|
||||
" - {name: MoveX, type: float, default: 0}\n"
|
||||
" - {name: MoveZ, type: float, default: 0}\n"
|
||||
" - {name: Speed, type: float, default: 0}\n"
|
||||
"States:\n"
|
||||
" Idle:\n"
|
||||
" clip: \"" +
|
||||
clips[0] + "\"\n"
|
||||
" loop: true\n"
|
||||
" Locomotion:\n"
|
||||
" blendTree:\n"
|
||||
" type: 2DDirectional\n"
|
||||
" x: MoveX\n"
|
||||
" z: MoveZ\n"
|
||||
" children:\n"
|
||||
" - {clip: \"" +
|
||||
clips[0] + "\", at: [0, 0]}\n"
|
||||
" - {clip: \"" +
|
||||
clipAt(1) + "\", at: [0, 1]}\n"
|
||||
" - {clip: \"" +
|
||||
clipAt(2) + "\", at: [1, 0]}\n"
|
||||
" - {clip: \"" +
|
||||
clipAt(3) + "\", at: [0, -1]}\n"
|
||||
" - {clip: \"" +
|
||||
clipAt(4) + "\", at: [-1, 0]}\n"
|
||||
"Transitions:\n"
|
||||
" - {from: Idle, to: Locomotion, cond: "
|
||||
"\"Speed > 0.1\", fade: 0.25}\n"
|
||||
" - {from: Locomotion, to: Idle, cond: "
|
||||
"\"Speed < 0.05\", fade: 0.3}\n";
|
||||
const std::string path =
|
||||
std::string("assets/animations/") +
|
||||
s_NewName + ".animctrl";
|
||||
std::filesystem::create_directories(
|
||||
"assets/animations");
|
||||
std::ofstream f(path, std::ios::binary |
|
||||
std::ios::trunc);
|
||||
s_CreateOk = static_cast<bool>(f);
|
||||
if (s_CreateOk)
|
||||
{
|
||||
f.write(yaml.data(),
|
||||
static_cast<std::streamsize>(
|
||||
yaml.size()));
|
||||
f.close();
|
||||
InvalidateControllerFiles();
|
||||
anim->controllerPath = path;
|
||||
anim->currentState.clear(); // 首帧回 entry
|
||||
ctx.assets->LoadController(path);
|
||||
// 参数默认值初始化(只补缺键,同槽指派)
|
||||
if (const auto* c =
|
||||
ctx.assets->FindController(path))
|
||||
{
|
||||
for (const auto& p : c->GetParams())
|
||||
{
|
||||
if (p.type ==
|
||||
Engine::Animation::
|
||||
ControllerParam::Type::Float)
|
||||
{
|
||||
anim->floatParams.emplace(
|
||||
p.name, p.defFloat);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (!anim->controllerPath.empty() &&
|
||||
ImGui::SmallButton("Reload"))
|
||||
{
|
||||
ctx.assets->ReloadController(
|
||||
anim->controllerPath);
|
||||
anim->currentState.clear(); // 回 entry
|
||||
}
|
||||
if (!s_CreateOk)
|
||||
{
|
||||
ImGui::TextColored(
|
||||
ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||
"! create failed (disk?)");
|
||||
}
|
||||
}
|
||||
if (ctrl == nullptr)
|
||||
{
|
||||
if (!anim->controllerPath.empty())
|
||||
{
|
||||
ImGui::TextColored(
|
||||
ImVec4(1.0f, 0.75f, 0.2f, 1.0f),
|
||||
"! controller failed to load");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("(no controller assigned)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto& order = ctrl->GetStateOrder();
|
||||
// ---- 当前状态(Parameters 之前:先看"现在在哪",
|
||||
// 再调参数)。过渡中显示 来源 -> 目标(百分比)----
|
||||
if (!anim->transFrom.empty())
|
||||
{
|
||||
const float pct =
|
||||
glm::min(anim->transElapsed /
|
||||
anim->transFade,
|
||||
1.0f) *
|
||||
100.0f;
|
||||
ImGui::Text("state: %s -> %s (%.0f%%)",
|
||||
anim->transFrom.c_str(),
|
||||
anim->currentState.c_str(), pct);
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Text("state: %s",
|
||||
anim->currentState.c_str());
|
||||
}
|
||||
// ---- Edit:结构编辑全在独立 Controller
|
||||
// Editor 面板(材质面板同范式——Details
|
||||
// 只留指派与运行状态)----
|
||||
if (ctx.controllerEditor != nullptr &&
|
||||
ImGui::SmallButton("Edit..."))
|
||||
{
|
||||
ctx.controllerEditor->Open(anim->controllerPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 依赖提示:动画作用在 MeshRenderer 的模型骨骼上
|
||||
if (mr == nullptr)
|
||||
{
|
||||
@@ -1002,6 +1673,13 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
}
|
||||
else
|
||||
{
|
||||
// ---- Skeleton 资产信息(只读)----骨骼数 = palette
|
||||
// 槽位数;节点数 = 蒙皮数学的全层级表(骨骼 ⊂ 节点,
|
||||
// 两数不同是正常的)。选中实体时视口内画骨架线框
|
||||
ImGui::Text("bones: %u nodes: %u",
|
||||
static_cast<unsigned>(model->GetBoneCount()),
|
||||
static_cast<unsigned>(model->GetNodes().size()));
|
||||
|
||||
// ---- Clips:动画资产槽(列全部模型容器文件——无法静态
|
||||
// 区分"模型 fbx"与"纯动画 fbx",一并列出,无 clip 的会
|
||||
// 警告跳过)----
|
||||
@@ -1029,14 +1707,67 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
{
|
||||
anim->attachedAnimPaths.push_back(path);
|
||||
}
|
||||
// attach 遥测(骨骼匹配结果,面板下方显示)
|
||||
s_LastAttachFile =
|
||||
std::filesystem::path(path).filename().string();
|
||||
s_LastAttachMatched =
|
||||
res.channelsMatched + res.channelsRetargeted;
|
||||
s_LastAttachRetargeted = res.channelsRetargeted;
|
||||
s_LastAttachSkipped = res.channelsSkipped;
|
||||
// 重定向明细快照(映射表调试显示)
|
||||
s_LastAttachRows = res.retargetRows;
|
||||
}
|
||||
// 已并入的源文件清单
|
||||
for (const auto& p : anim->attachedAnimPaths)
|
||||
// 已并入的源文件清单(x = detach:剥离模型 clip 表 +
|
||||
// 移除持久化列表;一帧只删一个,下帧重绘自然继续)
|
||||
for (size_t i = 0; i < anim->attachedAnimPaths.size(); ++i)
|
||||
{
|
||||
const auto& p = anim->attachedAnimPaths[i];
|
||||
ImGui::BulletText("%s", std::filesystem::path(p)
|
||||
.filename()
|
||||
.string()
|
||||
.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
const bool doDetach = ImGui::SmallButton("x");
|
||||
ImGui::PopID();
|
||||
if (doDetach)
|
||||
{
|
||||
ctx.assets->DetachAnimationClips(mr->model, p);
|
||||
anim->attachedAnimPaths.erase(
|
||||
anim->attachedAnimPaths.begin() +
|
||||
static_cast<ptrdiff_t>(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 最近一次 attach 的骨骼匹配遥测(同名直映 + Humanoid
|
||||
// 槽位重定向分列;跳过 = 手指/道具等无对应骨骼)。
|
||||
// 注意:重启后自愈重放路径幂等命中返回全 0,那里不更新
|
||||
// 遥测避免误导
|
||||
if (!s_LastAttachFile.empty())
|
||||
{
|
||||
ImGui::TextColored(
|
||||
ImVec4(0.55f, 0.75f, 1.0f, 1.0f),
|
||||
"%s: %u matched (%u retargeted), %u skipped",
|
||||
s_LastAttachFile.c_str(),
|
||||
static_cast<unsigned>(s_LastAttachMatched),
|
||||
static_cast<unsigned>(s_LastAttachRetargeted),
|
||||
static_cast<unsigned>(s_LastAttachSkipped));
|
||||
}
|
||||
// 重定向明细表(调试黑盒白盒化):源骨骼 → 槽位 → 目标
|
||||
// 骨骼。行错了 = 哪根骨乱转一眼可查(Unity Configure
|
||||
// Avatar 的只读版)
|
||||
if (!s_LastAttachRows.empty())
|
||||
{
|
||||
if (ImGui::TreeNode("Retarget Map"))
|
||||
{
|
||||
for (const auto& r : s_LastAttachRows)
|
||||
{
|
||||
ImGui::BulletText("%s [%s] -> %s",
|
||||
r.srcName.c_str(), r.slot,
|
||||
r.dstName.c_str());
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
if (!model->HasAnimations())
|
||||
{
|
||||
@@ -1045,9 +1776,18 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
}
|
||||
if (model != nullptr && model->HasAnimations())
|
||||
{
|
||||
// ---- Playback:播放控制(切换 clip 触发 crossfade)----
|
||||
ImGui::SeparatorText("Playback");
|
||||
const auto& clips = model->GetClips();
|
||||
// ---- Playback:播放控制。Direct 专属控件(clip 选择/
|
||||
// playing/loop/时间拖动条)只在 Direct 模式渲染——
|
||||
// Controller 模式下播放由状态机调度,这些字段不参
|
||||
// 与求值(Unity Animator 语义:Controller 挂上后
|
||||
// Animation 组件的 clip 字段灰置)----
|
||||
const bool ctrlMode =
|
||||
anim->mode ==
|
||||
Engine::ECS::AnimationComponent::AnimMode::Controller;
|
||||
if (!ctrlMode)
|
||||
{
|
||||
ImGui::SeparatorText("Playback");
|
||||
std::vector<const char*> clipNames;
|
||||
clipNames.reserve(clips.size());
|
||||
for (const auto& c : clips)
|
||||
@@ -1063,14 +1803,34 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
|
||||
ImGui::Checkbox("Playing", &anim->playing);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Loop", &anim->loop);
|
||||
if (anim->clipIndex >= 0 &&
|
||||
anim->clipIndex <
|
||||
static_cast<int>(clips.size()))
|
||||
{
|
||||
// 时间拖动条(Unity Animation 窗口 scrub 语义):
|
||||
// 拖住定格播放头逐帧看姿态,松手恢复拖前播放状
|
||||
// 态。暂停时 AnimationSystem 照样求值(只有
|
||||
// playing 才推进播放头),所以拖动即实时预览
|
||||
const float dur =
|
||||
clips[anim->clipIndex].duration;
|
||||
ImGui::SliderFloat("Time", &anim->time, 0.0f, dur,
|
||||
"t: %.2f / %.2f s");
|
||||
if (ImGui::IsItemActivated())
|
||||
{
|
||||
s_ScrubWasPlaying = anim->playing;
|
||||
anim->playing = false;
|
||||
}
|
||||
if (ImGui::IsItemDeactivatedAfterEdit() &&
|
||||
s_ScrubWasPlaying)
|
||||
{
|
||||
anim->playing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 两模式通用:全局速度 / 过渡时长(Controller 的
|
||||
// CrossFade 与 Direct 的 crossfade 共用 fade)
|
||||
ImGui::SliderFloat("Speed", &anim->speed, -2.0f, 2.0f);
|
||||
ImGui::SliderFloat("Fade", &anim->fadeDuration, 0.0f, 1.0f);
|
||||
if (anim->clipIndex >= 0 &&
|
||||
anim->clipIndex < static_cast<int>(clips.size()))
|
||||
{
|
||||
ImGui::Text("t: %.2f / %.2f s", anim->time,
|
||||
clips[anim->clipIndex].duration);
|
||||
}
|
||||
// 动画数据表:每个 clip 的时长与有效轨道数(attach 进来的
|
||||
// 外部动画在此核对骨骼匹配结果——轨道数明显偏少 = 骨骼
|
||||
// 命名不同源被跳过)
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
//
|
||||
// MaterialAssetUtil.h:材质资产编辑共用工具(Inspector / Material Editor
|
||||
// 面板共享)。
|
||||
//
|
||||
// 单一事实来源(原 InspectorPanel.cpp 匿名 ns 实现上提):
|
||||
// - .material 文件枚举缓存(inline 变量跨 TU 唯一——面板各自创建
|
||||
// Override 后缓存失效即时可见)
|
||||
// - Create Override:内建材质快照 → assets/materials/*.material →
|
||||
// GUID 登记 → 同步加载 → 挂实体覆盖表
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_MATERIALASSETUTIL_H
|
||||
#define HELLOVULKAN_MATERIALASSETUTIL_H
|
||||
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Renderer/MaterialAsset.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace EditorMaterialUtil {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// .material 文件枚举(会话级缓存 + 脏标)。inline 变量 = 所有 TU
|
||||
// 共享同一份(Create Override 新建文件后置脏重扫)
|
||||
inline std::vector<std::string> s_MaterialFiles;
|
||||
inline bool s_MaterialFilesDirty = true;
|
||||
|
||||
inline void InvalidateMaterialFileList() { s_MaterialFilesDirty = true; }
|
||||
|
||||
// 递归收集 assets/ 下纹理文件(材质编辑器纹理槽选择器;纹理文件
|
||||
// 不被编辑器创建,无需脏标)。会话级缓存
|
||||
inline const std::vector<std::string>& AssetTextureFiles()
|
||||
{
|
||||
static std::vector<std::string> s_Files;
|
||||
static bool s_Cached = false;
|
||||
if (!s_Cached)
|
||||
{
|
||||
s_Cached = true;
|
||||
if (fs::exists("assets"))
|
||||
{
|
||||
for (const auto& entry :
|
||||
fs::recursive_directory_iterator("assets"))
|
||||
{
|
||||
if (!entry.is_regular_file())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::string ext = entry.path().extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c)
|
||||
{ return std::tolower(c); });
|
||||
if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" ||
|
||||
ext == ".tga" || ext == ".bmp" || ext == ".hdr")
|
||||
{
|
||||
s_Files.push_back(entry.path().generic_string());
|
||||
}
|
||||
}
|
||||
std::sort(s_Files.begin(), s_Files.end());
|
||||
}
|
||||
}
|
||||
return s_Files;
|
||||
}
|
||||
|
||||
inline const std::vector<std::string>& AssetMaterialFiles()
|
||||
{
|
||||
if (s_MaterialFilesDirty)
|
||||
{
|
||||
s_MaterialFilesDirty = false;
|
||||
s_MaterialFiles.clear();
|
||||
if (fs::exists("assets"))
|
||||
{
|
||||
for (const auto& entry :
|
||||
fs::recursive_directory_iterator("assets"))
|
||||
{
|
||||
if (!entry.is_regular_file())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
std::string ext = entry.path().extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(),
|
||||
[](unsigned char c)
|
||||
{ return std::tolower(c); });
|
||||
if (ext == ".material")
|
||||
{
|
||||
s_MaterialFiles.push_back(
|
||||
entry.path().generic_string());
|
||||
}
|
||||
}
|
||||
std::sort(s_MaterialFiles.begin(), s_MaterialFiles.end());
|
||||
}
|
||||
}
|
||||
return s_MaterialFiles;
|
||||
}
|
||||
|
||||
// Create Override:内建材质快照 → assets/materials/<model>_<mesh>.material
|
||||
// → GUID 登记 → 同步加载 → 挂实体覆盖表。失败返回空串(不挂槽)。
|
||||
// 数据段 = 内建材质只读快照(纹理路径 + 因子 + 透明语义),此后
|
||||
// 该资产独立演化,内建材质不受影响
|
||||
inline std::string CreateMaterialOverride(
|
||||
const std::string& modelPath,
|
||||
const Engine::MeshMaterialInfo& info,
|
||||
Engine::u32 meshIdx,
|
||||
Engine::ECS::MeshRendererComponent& mr,
|
||||
Engine::Assets::AssetManager& assets)
|
||||
{
|
||||
// 目标文件名:<modelStem>_<meshName|idx>.material;mesh 名非
|
||||
// [A-Za-z0-9] 字符折叠 '_',空名回退下标;重名追加 _2/_3/...
|
||||
std::string stem = fs::path(modelPath).stem().string();
|
||||
std::string leaf = info.name;
|
||||
if (leaf.empty())
|
||||
{
|
||||
leaf = std::to_string(meshIdx);
|
||||
}
|
||||
for (char& c : leaf)
|
||||
{
|
||||
if (!std::isalnum(static_cast<unsigned char>(c)))
|
||||
{
|
||||
c = '_';
|
||||
}
|
||||
}
|
||||
|
||||
const fs::path dir = "assets/materials";
|
||||
std::error_code ec;
|
||||
fs::create_directories(dir, ec);
|
||||
if (ec)
|
||||
{
|
||||
ENGINE_WARN("MaterialUtil: cannot create {}: {}", dir.string(),
|
||||
ec.message());
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string base = stem + "_" + leaf;
|
||||
std::string rel = (dir / (base + ".material")).generic_string();
|
||||
for (int n = 2; fs::exists(rel); ++n)
|
||||
{
|
||||
rel = (dir / (base + "_" + std::to_string(n) + ".material"))
|
||||
.generic_string();
|
||||
}
|
||||
|
||||
Engine::MaterialAssetData data;
|
||||
data.texPaths = info.texPaths;
|
||||
data.params = info.params;
|
||||
data.transparent = info.transparent;
|
||||
if (!Engine::MaterialAsset::Save(rel, data))
|
||||
{
|
||||
ENGINE_WARN("MaterialUtil: failed to save material override '{}'",
|
||||
rel);
|
||||
return {};
|
||||
}
|
||||
|
||||
assets.GetOrCreateGUID(rel);
|
||||
if (!assets.LoadMaterialAsset(rel).Valid())
|
||||
{
|
||||
ENGINE_WARN("MaterialUtil: override '{}' saved but failed to load",
|
||||
rel);
|
||||
return {};
|
||||
}
|
||||
mr.materialOverrides[meshIdx] = rel;
|
||||
InvalidateMaterialFileList(); // 新文件进下拉列表
|
||||
return rel;
|
||||
}
|
||||
|
||||
} // namespace EditorMaterialUtil
|
||||
|
||||
#endif //HELLOVULKAN_MATERIALASSETUTIL_H
|
||||
@@ -0,0 +1,321 @@
|
||||
//
|
||||
// MaterialEditorPanel.cpp:独立材质编辑器实现。
|
||||
//
|
||||
// 编辑语义原样承接自原 Inspector "Edit Material" 内嵌块(行为不变的
|
||||
// 搬家):参数 EditParams 直写 UBO + IsItemDeactivatedAfterEdit 落盘;
|
||||
// 纹理槽改动落盘 + ReloadMaterialAsset 重建(旧指针即刻悬空,本帧
|
||||
// 不再触碰 asset)。
|
||||
//
|
||||
|
||||
#include "MaterialEditorPanel.h"
|
||||
#include "MaterialAssetUtil.h"
|
||||
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
|
||||
namespace {
|
||||
|
||||
// InputText 需要 char 缓冲;提交时写回 std::string(与 Inspector 同款)
|
||||
bool InputTextString(const char* label, std::string& str)
|
||||
{
|
||||
char buf[256];
|
||||
std::snprintf(buf, sizeof(buf), "%s", str.c_str());
|
||||
if (ImGui::InputText(label, buf, sizeof(buf)))
|
||||
{
|
||||
str = buf;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr float kPreviewSize = 128.0f; // 编辑器顶部大球(Inspector 槽位为 48²)
|
||||
} // namespace
|
||||
|
||||
void MaterialEditorPanel::OpenAsset(const std::string& path)
|
||||
{
|
||||
m_AssetMode = true;
|
||||
m_Path = path;
|
||||
m_LoadTried = false;
|
||||
m_Owner = entt::null;
|
||||
m_Open = true;
|
||||
}
|
||||
|
||||
void MaterialEditorPanel::OpenBuiltin(entt::entity owner, Engine::u32 meshIdx,
|
||||
const std::string& displayName)
|
||||
{
|
||||
m_AssetMode = false;
|
||||
m_Owner = owner;
|
||||
m_MeshIdx = meshIdx;
|
||||
m_DisplayName = displayName;
|
||||
m_Path.clear();
|
||||
m_Open = true;
|
||||
}
|
||||
|
||||
void MaterialEditorPanel::Draw(const EditorContext& ctx)
|
||||
{
|
||||
if (!m_Open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!ImGui::Begin("Material Editor", &m_Open))
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_AssetMode)
|
||||
{
|
||||
DrawAssetMode(ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
DrawBuiltinMode(ctx);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
// 顶部预览球(tile 池请求幂等——窗口开着才请求,池按可见项换页)
|
||||
void MaterialEditorPanel::DrawPreview(const EditorContext& ctx,
|
||||
Engine::Material* mat)
|
||||
{
|
||||
Engine::i32 tile = -1;
|
||||
void* atlasTex = nullptr;
|
||||
if (ctx.renderer && mat)
|
||||
{
|
||||
tile = ctx.renderer->RequestMaterialPreview(mat);
|
||||
atlasTex = ctx.renderer->GetMaterialPreviewTexture();
|
||||
}
|
||||
if (atlasTex && tile >= 0)
|
||||
{
|
||||
const float uvStep = 1.0f / float(Engine::Passes::kPreviewTilesPerSide);
|
||||
const float u0 =
|
||||
float(tile % Engine::Passes::kPreviewTilesPerSide) * uvStep;
|
||||
const float v0 =
|
||||
float(tile / Engine::Passes::kPreviewTilesPerSide) * uvStep;
|
||||
ImGui::Image(atlasTex, ImVec2(kPreviewSize, kPreviewSize),
|
||||
ImVec2(u0, v0), ImVec2(u0 + uvStep, v0 + uvStep));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::Dummy(ImVec2(kPreviewSize, kPreviewSize));
|
||||
}
|
||||
}
|
||||
|
||||
void MaterialEditorPanel::DrawAssetMode(const EditorContext& ctx)
|
||||
{
|
||||
if (!ctx.assets)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 每帧重解析:ReloadMaterialAsset 换实例 / 未加载自动 Load 一次
|
||||
Engine::MaterialAsset* asset = ctx.assets->FindMaterialAsset(m_Path);
|
||||
if (!asset && !m_LoadTried)
|
||||
{
|
||||
m_LoadTried = true;
|
||||
ctx.assets->LoadMaterialAsset(m_Path);
|
||||
asset = ctx.assets->FindMaterialAsset(m_Path);
|
||||
}
|
||||
if (!asset)
|
||||
{
|
||||
ImGui::TextDisabled("material not loaded: %s", m_Path.c_str());
|
||||
if (ImGui::Button("Retry Load"))
|
||||
{
|
||||
m_LoadTried = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string& path = m_Path;
|
||||
const std::string name =
|
||||
std::filesystem::path(path).filename().generic_string();
|
||||
|
||||
// ---- 头部:预览球 + 资产名/路径 ----
|
||||
DrawPreview(ctx, asset->GetMaterial());
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
ImGui::TextUnformatted(name.c_str());
|
||||
ImGui::TextDisabled("%s", path.c_str());
|
||||
ImGui::TextDisabled("shared asset: edits apply to all entities "
|
||||
"referencing it");
|
||||
ImGui::EndGroup();
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- 透明队列归属(收集侧 CPU 标志)----
|
||||
bool trans = asset->GetMaterial()->IsTransparent();
|
||||
if (ImGui::Checkbox("Transparent", &trans))
|
||||
{
|
||||
asset->SetTransparent(trans);
|
||||
Engine::MaterialAsset::Save(path, asset->GetData());
|
||||
}
|
||||
|
||||
// ---- 参数(变化直写 UBO 实时预览;松手落盘,拖拽中不产生每帧 IO)----
|
||||
Engine::MaterialParams p = asset->GetData().params;
|
||||
auto EditParam = [&](bool changed)
|
||||
{
|
||||
if (changed)
|
||||
{
|
||||
asset->EditParams(p);
|
||||
}
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
Engine::MaterialAsset::Save(path, asset->GetData());
|
||||
}
|
||||
};
|
||||
EditParam(
|
||||
ImGui::DragFloat("Metallic", &p.factors.x, 0.01f, 0.0f, 1.0f));
|
||||
EditParam(
|
||||
ImGui::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f));
|
||||
EditParam(ImGui::ColorEdit4("Base Color", &p.baseColor.x));
|
||||
EditParam(ImGui::DragFloat("IOR", &p.surface.x, 0.01f, 1.0f, 3.0f));
|
||||
EditParam(ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f,
|
||||
1.0f));
|
||||
EditParam(ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f,
|
||||
0.0f, 100.0f));
|
||||
EditParam(ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f,
|
||||
10.0f));
|
||||
|
||||
// ---- 纹理槽(变化 → 落盘 + waitIdle 重建)----
|
||||
ImGui::SeparatorText("Textures");
|
||||
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
|
||||
"Albedo", "Normal", "MetallicRoughness",
|
||||
"Emissive", "Ao", "Roughness"};
|
||||
int pendingSlot = -1;
|
||||
std::string pendingPath;
|
||||
for (Engine::u32 s = 0; s < Engine::Material::kSlotCount; ++s)
|
||||
{
|
||||
std::string tp = asset->GetData().texPaths[s];
|
||||
ImGui::PushID(static_cast<int>(s));
|
||||
InputTextString(kSlotNames[s], tp);
|
||||
if (ImGui::IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
pendingSlot = static_cast<int>(s);
|
||||
pendingPath = tp;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("..."))
|
||||
{
|
||||
ImGui::OpenPopup("tex_pick");
|
||||
}
|
||||
if (ImGui::BeginPopup("tex_pick"))
|
||||
{
|
||||
for (const auto& f : EditorMaterialUtil::AssetTextureFiles())
|
||||
{
|
||||
if (ImGui::Selectable(f.c_str()))
|
||||
{
|
||||
pendingSlot = static_cast<int>(s);
|
||||
pendingPath = f;
|
||||
}
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (pendingSlot >= 0)
|
||||
{
|
||||
// 拷出数据段 → 落盘 → 重建资产(旧指针即刻悬空:本帧到此不再
|
||||
// 触碰 asset,下帧 Find 取新实例)。旧材质已在本帧 Request 进
|
||||
// 预览池脏表(新 tile 未画满三槽位时每帧重入队)——先 drop 再
|
||||
// 销毁,否则 Render 段解引用悬空指针
|
||||
Engine::MaterialAssetData d = asset->GetData();
|
||||
d.texPaths[static_cast<Engine::u32>(pendingSlot)] = pendingPath;
|
||||
Engine::MaterialAsset::Save(path, d);
|
||||
if (ctx.renderer)
|
||||
{
|
||||
ctx.renderer->DropMaterialPreview(asset->GetMaterial());
|
||||
}
|
||||
ctx.assets->ReloadMaterialAsset(path);
|
||||
}
|
||||
}
|
||||
|
||||
void MaterialEditorPanel::DrawBuiltinMode(const EditorContext& ctx)
|
||||
{
|
||||
// 每帧重解析:实体可销毁(PIE Stop 换血)/ 模型可卸载
|
||||
if (!ctx.scene || !ctx.assets)
|
||||
{
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
auto* mr = reg.valid(m_Owner)
|
||||
? reg.try_get<Engine::ECS::MeshRendererComponent>(m_Owner)
|
||||
: nullptr;
|
||||
const Engine::Model* model =
|
||||
(mr && mr->model.Valid()) ? ctx.assets->GetModel(mr->model) : nullptr;
|
||||
const Engine::MeshMaterialInfo* info =
|
||||
(model && m_MeshIdx < model->GetMeshMaterialInfos().size())
|
||||
? &model->GetMeshMaterialInfos()[m_MeshIdx]
|
||||
: nullptr;
|
||||
if (!mr || !model || !info)
|
||||
{
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
|
||||
"built-in material no longer available");
|
||||
ImGui::TextDisabled("(entity destroyed or model unloaded)");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string meshName =
|
||||
(!info->name.empty())
|
||||
? info->name
|
||||
: "Slot " + std::to_string(m_MeshIdx);
|
||||
|
||||
// ---- 头部:预览球 + 名称/来源 ----
|
||||
DrawPreview(ctx, model->GetMeshes()[m_MeshIdx]->GetMaterial());
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginGroup();
|
||||
ImGui::TextUnformatted(meshName.c_str());
|
||||
ImGui::TextDisabled("built-in material of %s",
|
||||
std::filesystem::path(
|
||||
ctx.assets->Models().Path(mr->model))
|
||||
.filename()
|
||||
.generic_string()
|
||||
.c_str());
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f),
|
||||
"read-only: built-in materials belong to the model");
|
||||
ImGui::EndGroup();
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- 一键转可编辑:Create Override → 自动切到资产模式 ----
|
||||
if (ImGui::Button("Create Override"))
|
||||
{
|
||||
const std::string rel = EditorMaterialUtil::CreateMaterialOverride(
|
||||
ctx.assets->Models().Path(mr->model), *info, m_MeshIdx, *mr,
|
||||
*ctx.assets);
|
||||
if (!rel.empty())
|
||||
{
|
||||
OpenAsset(rel); // 数据已快照进资产,无缝进入编辑
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
// ---- 只读展示(与资产模式同款控件,锁编辑看值)----
|
||||
ImGui::BeginDisabled(true);
|
||||
bool trans = info->transparent;
|
||||
ImGui::Checkbox("Transparent", &trans);
|
||||
Engine::MaterialParams p = info->params;
|
||||
ImGui::DragFloat("Metallic", &p.factors.x, 0.01f, 0.0f, 1.0f);
|
||||
ImGui::DragFloat("Roughness", &p.factors.y, 0.01f, 0.0f, 1.0f);
|
||||
ImGui::ColorEdit4("Base Color", &p.baseColor.x);
|
||||
ImGui::DragFloat("IOR", &p.surface.x, 0.01f, 1.0f, 3.0f);
|
||||
ImGui::DragFloat("Transmission", &p.surface.y, 0.01f, 0.0f, 1.0f);
|
||||
ImGui::DragFloat("Emissive Strength", &p.surface.z, 0.05f, 0.0f, 100.0f);
|
||||
ImGui::DragFloat("Thickness", &p.surface.w, 0.01f, 0.0f, 10.0f);
|
||||
ImGui::SeparatorText("Textures");
|
||||
static constexpr const char* kSlotNames[Engine::Material::kSlotCount] = {
|
||||
"Albedo", "Normal", "MetallicRoughness",
|
||||
"Emissive", "Ao", "Roughness"};
|
||||
for (Engine::u32 s = 0; s < Engine::Material::kSlotCount; ++s)
|
||||
{
|
||||
std::string tp = info->texPaths[s];
|
||||
ImGui::PushID(static_cast<int>(s));
|
||||
InputTextString(kSlotNames[s], tp);
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// MaterialEditorPanel:独立材质编辑器窗口(UE Material Editor 语义)。
|
||||
//
|
||||
// 与 Details 面板职责分离:Details 材质槽行只做"指派"(换绑/还原/拖拽
|
||||
// 覆盖资产),双击槽位预览球或 Content Browser 的 .material 条目 →
|
||||
// 本面板打开做"编辑"(参数全平铺 + 纹理槽)。
|
||||
//
|
||||
// 两种模式:
|
||||
// - 资产模式(可编辑):编辑 .material 资产。参数走
|
||||
// EditParams 直写 UBO(松手落盘),纹理槽变化走 ReloadMaterialAsset
|
||||
// (waitIdle 全停重建,低频路径)——与原 Inspector 内嵌编辑完全
|
||||
// 同一套语义,此处原样搬运
|
||||
// - 内建模式(只读):Model 内建材质无 CPU 参数副本(直写 GPU UBO),
|
||||
// 只读展示 MeshMaterialInfo 快照 + 站内一键 Create Override(生成
|
||||
// 资产后自动切换到资产模式编辑)
|
||||
//
|
||||
// 打开目标每帧重解析(实体可销毁 / 模型可卸载 / 资产可 Reload 换实例),
|
||||
// 面板不跨帧持有任何指针。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_MATERIALEDITORPANEL_H
|
||||
#define HELLOVULKAN_MATERIALEDITORPANEL_H
|
||||
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include <Engine/Core/Types.h>
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
#include <string>
|
||||
|
||||
// 材质资产拖拽 payload(Content Browser 拖 .material → Details 槽位)
|
||||
inline constexpr const char* kMaterialDragPayload = "MAT_PATH";
|
||||
|
||||
class MaterialEditorPanel {
|
||||
public:
|
||||
// 打开 .material 资产(可编辑模式)
|
||||
void OpenAsset(const std::string& path);
|
||||
// 打开内建材质(只读模式):owner 实体 + mesh 槽位每帧重解析
|
||||
void OpenBuiltin(entt::entity owner, Engine::u32 meshIdx,
|
||||
const std::string& displayName);
|
||||
|
||||
void Draw(const EditorContext& ctx);
|
||||
|
||||
private:
|
||||
void DrawPreview(const EditorContext& ctx, Engine::Material* mat);
|
||||
void DrawAssetMode(const EditorContext& ctx);
|
||||
void DrawBuiltinMode(const EditorContext& ctx);
|
||||
|
||||
bool m_Open = false;
|
||||
bool m_AssetMode = false;
|
||||
// 资产模式目标
|
||||
std::string m_Path;
|
||||
bool m_LoadTried = false; // 未加载时自动 Load 一次(防每帧重试)
|
||||
// 内建模式目标
|
||||
entt::entity m_Owner{entt::null};
|
||||
Engine::u32 m_MeshIdx = 0;
|
||||
std::string m_DisplayName;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_MATERIALEDITORPANEL_H
|
||||
@@ -143,6 +143,83 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
else size.y = avail.x / aspect;
|
||||
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
|
||||
|
||||
// ---- 骨骼关节 overlay(Endfield-Poser 范式):3D 骨架线
|
||||
// 给结构,屏幕圆点给可读性。世界→屏幕与渲染同相机,
|
||||
// Vulkan NDC y 向下与 ImGui 屏幕坐标同构 → 线性映射
|
||||
// image 矩形即可。hover 关节 = 白圈高亮 + 骨骼名
|
||||
{
|
||||
const auto &frameData = renderer.GetFrameSceneData();
|
||||
if (frameData.showBoneWires && !frameData.pieActive &&
|
||||
!frameData.boneJoints.empty() &&
|
||||
ctx.editorCamera != nullptr) {
|
||||
const ImVec2 rmin = ImGui::GetItemRectMin();
|
||||
const ImVec2 rmax = ImGui::GetItemRectMax();
|
||||
const glm::mat4 vp =
|
||||
ctx.editorCamera->GetController().GetProjection(aspect) *
|
||||
ctx.editorCamera->GetView();
|
||||
ImDrawList *dl = ImGui::GetWindowDrawList();
|
||||
dl->PushClipRect(rmin, rmax, true);
|
||||
const f32 r = frameData.boneJointPx;
|
||||
const ImGuiIO &io = ImGui::GetIO();
|
||||
const ImVec2 mouse = io.MousePos;
|
||||
int hovered = -1;
|
||||
float hoveredD = 0.0f;
|
||||
for (size_t ji = 0; ji < frameData.boneJoints.size(); ++ji) {
|
||||
const auto &jm = frameData.boneJoints[ji];
|
||||
const glm::vec4 c = vp * glm::vec4(jm.pos, 1.0f);
|
||||
if (c.w <= 0.0f) continue; // 相机身后
|
||||
const ImVec2 p(
|
||||
rmin.x + (c.x / c.w * 0.5f + 0.5f) * size.x,
|
||||
rmin.y + (c.y / c.w * 0.5f + 0.5f) * size.y);
|
||||
// 骨骼 = 蓝(Endfield humanoid 色),普通节点 = 青
|
||||
const ImU32 fill = jm.isBone
|
||||
? IM_COL32(70, 160, 255, 230)
|
||||
: IM_COL32(70, 190, 190, 190);
|
||||
dl->AddCircleFilled(p, r, fill);
|
||||
dl->AddCircle(p, r + 1.5f,
|
||||
IM_COL32(255, 255, 255, 140), 0, 1.2f);
|
||||
// hover 命中(半径 +3px 容差,取最近)
|
||||
const float dx = p.x - mouse.x, dy = p.y - mouse.y;
|
||||
const float d = dx * dx + dy * dy;
|
||||
const float hit = (r + 3.0f) * (r + 3.0f);
|
||||
if (d <= hit && (hovered < 0 || d < hoveredD)) {
|
||||
hovered = static_cast<int>(ji);
|
||||
hoveredD = d;
|
||||
}
|
||||
}
|
||||
// hover 高亮圈 + 骨骼名 tooltip(名字查选中实体模型
|
||||
// 节点表;查不到 = 显示节点下标)
|
||||
if (hovered >= 0 && ImGui::IsItemHovered()) {
|
||||
const auto &jm = frameData.boneJoints[hovered];
|
||||
const glm::vec4 c = vp * glm::vec4(jm.pos, 1.0f);
|
||||
const ImVec2 p(
|
||||
rmin.x + (c.x / c.w * 0.5f + 0.5f) * size.x,
|
||||
rmin.y + (c.y / c.w * 0.5f + 0.5f) * size.y);
|
||||
dl->AddCircle(p, r + 3.5f,
|
||||
IM_COL32(255, 255, 255, 255), 0, 2.0f);
|
||||
const char *name = nullptr;
|
||||
if (selected != entt::null) {
|
||||
// 本段在 reg 声明之前,就地取 registry
|
||||
const auto *mrSel =
|
||||
ctx.scene->GetRegistry().try_get<
|
||||
Engine::ECS::MeshRendererComponent>(
|
||||
selected);
|
||||
if (mrSel && mrSel->model.Valid()) {
|
||||
const Model *mdl =
|
||||
ctx.assets->GetModel(mrSel->model);
|
||||
if (mdl && jm.nodeIdx < mdl->GetNodes().size())
|
||||
name = mdl->GetNodes()[jm.nodeIdx]
|
||||
.name.c_str();
|
||||
}
|
||||
}
|
||||
ImGui::SetTooltip(
|
||||
"%s%s", name ? name : "(node)",
|
||||
jm.isBone ? "" : " [non-bone]");
|
||||
}
|
||||
dl->PopClipRect();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 工具热键(UE 语义):W/E/R 切 Gizmo 模式、T 切
|
||||
// local/world、F 焦点选中实体。仅 Viewport 持焦时生效——
|
||||
// 输入框打字(WantTextInput)由 BlockingKeyboard 拦游戏侧,
|
||||
@@ -163,11 +240,12 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_E)) gizmo.op = ImGuizmo::ROTATE;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_T)) gizmo.localSpace = !gizmo.localSpace;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F) && tr != nullptr) {
|
||||
const bool fCombo = ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift;
|
||||
// F = 焦点选中实体(frame-selection):编辑器相机
|
||||
// 环绕 pivot 设为选中实体位置,沿视线平移对准(环
|
||||
// 绕距离沿用当前值,滚轮可调)。世界位姿(层级
|
||||
// 实体 = 父链组合结果)
|
||||
// 实体 = 父链组合结果)。裸 F 才触发(组合键排他)
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F) && !fCombo && tr != nullptr) {
|
||||
ctx.editorCamera->FocusOn(tr->GetWorldPosition());
|
||||
}
|
||||
}
|
||||
@@ -179,14 +257,13 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
if (tr != nullptr && !playing) {
|
||||
// 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏
|
||||
// 形态 = primary 相机实体;与 SceneRenderer::BuildFrame
|
||||
// 同一分派),参数与渲染侧一致(aspect = 渲染分辨率、
|
||||
// near/far 0.1/100)
|
||||
// 同一分派),投影参数由相机自身承载(与渲染侧一致)
|
||||
ECS::TransformComponent *camT = nullptr;
|
||||
if (Engine::Camera *cam = ViewCamera(ctx, camT);
|
||||
cam != nullptr && camT != nullptr) {
|
||||
const glm::mat4 view = cam->GetView(*camT);
|
||||
const glm::mat4 projGL = ProjVulkanToGL(
|
||||
cam->GetProjection(aspect, 0.1f, 100.0f));
|
||||
cam->GetProjection(aspect));
|
||||
|
||||
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
|
||||
// 杆画在 Image 区域内(等比适配后的 item rect)
|
||||
@@ -285,7 +362,7 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
// Gizmo 状态提示(左上角 drawlist 叠加,原因同下:不占布局行)
|
||||
// PIE 运行态 = 黄色 PLAYING/PAUSED 提示替代编辑状态
|
||||
{
|
||||
char tip[64];
|
||||
char tip[128];
|
||||
if (playing) {
|
||||
const bool paused = ctx.pausedState && *ctx.pausedState;
|
||||
std::snprintf(tip, sizeof(tip),
|
||||
@@ -293,7 +370,8 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
|
||||
: "PLAYING - Ctrl+P to stop");
|
||||
} else {
|
||||
std::snprintf(tip, sizeof(tip),
|
||||
"Space: %s [T] Snap: Ctrl",
|
||||
"Space: %s [T] Snap: Ctrl F: Focus "
|
||||
"Ctrl+Shift+F: To Selection",
|
||||
gizmo.localSpace ? "Local" : "World");
|
||||
}
|
||||
const ImVec2 imgMin = ImGui::GetItemRectMin();
|
||||
|
||||
@@ -26,8 +26,12 @@
|
||||
|
||||
#include <Engine/Core/Input.h>
|
||||
|
||||
// editor/game 编译期分离(HELLOVULKAN_EDITOR CMake option):editor 形态
|
||||
// 挂接 docking Editor;game 形态 = Direct 直渲 + primary 相机,零编辑器代码
|
||||
#ifdef HELLO_EDITOR
|
||||
#include "Editor.h"
|
||||
#include "EditorContext.h"
|
||||
#endif
|
||||
|
||||
using namespace Engine;
|
||||
|
||||
@@ -51,6 +55,7 @@ namespace
|
||||
void OnInit() override
|
||||
{
|
||||
Renderer& renderer = GetRenderer();
|
||||
#ifdef HELLO_EDITOR
|
||||
renderer.SetDisplayMode(kDisplayViewport);
|
||||
|
||||
|
||||
@@ -59,10 +64,15 @@ namespace
|
||||
|
||||
// Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
|
||||
InitEditor();
|
||||
#else
|
||||
// game 形态:全窗口 Direct 直渲(Renderer 默认即 kDisplayDirect),
|
||||
// 相机由场景 primary camera 提供,无 UI
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnUpdate(const FrameTime& frameTime) override
|
||||
{
|
||||
#ifdef HELLO_EDITOR
|
||||
// 编辑器形态:驱动编辑器相机(RMB 飞行 / MMB 焦点环绕 /
|
||||
// 滚轮推拉)。暂停(timeScale=0)时仍可飞行——unscaled dt。
|
||||
// PIE 内嵌播放:渲染源已切 primary 相机,编辑器相机停驱
|
||||
@@ -73,31 +83,42 @@ namespace
|
||||
{
|
||||
GetRenderer().GetEditorCamera().Update(frameTime.UnscaledDt());
|
||||
}
|
||||
#else
|
||||
(void)frameTime; // game 形态:本沙盒无游戏逻辑
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- UI(全部委托 docking Editor;本文件零 ImGui 控件)----
|
||||
|
||||
void OnImGuiRender() override
|
||||
{
|
||||
#ifdef HELLO_EDITOR
|
||||
m_Editor->Draw();
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---- 事件 ----
|
||||
|
||||
void OnWindowClose(const Event& event) override
|
||||
{
|
||||
#ifdef HELLO_EDITOR
|
||||
// 退出确认走 Editor 的模态框(ImGui 必须可见才有 UI)
|
||||
SetImGuiEnabled(true);
|
||||
m_Editor->RequestQuit();
|
||||
#else
|
||||
(void)event;
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnKeyPress(const Event& event) override
|
||||
{
|
||||
// 键码统一走 Input::Key(App 层零 GLFW 依赖)
|
||||
#ifdef HELLO_EDITOR
|
||||
if (event.key == static_cast<int>(Input::Key::GraveAccent))
|
||||
{
|
||||
SetImGuiEnabled(!GetImGuiEnabled());
|
||||
}
|
||||
#endif
|
||||
if (event.key == static_cast<int>(Input::Key::F1))
|
||||
{
|
||||
STATS.PrintComparison();
|
||||
@@ -111,6 +132,7 @@ namespace
|
||||
}
|
||||
|
||||
private:
|
||||
#ifdef HELLO_EDITOR
|
||||
// Editor 注入:门面指针 + 反向能力回调(面板 → 沙盒)
|
||||
void InitEditor()
|
||||
{
|
||||
@@ -136,6 +158,7 @@ namespace
|
||||
|
||||
private:
|
||||
std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>();
|
||||
#endif
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,12 @@ add_subdirectory(Lib/yaml-cpp)
|
||||
# Vulkan
|
||||
find_package(Vulkan REQUIRED)
|
||||
|
||||
# ---- editor/game 编译期分离(UE WITH_EDITOR / Unity UNITY_EDITOR 模式)----
|
||||
# ON(默认):HELLO_EDITOR=1,材质球预览 tile 池 + App/editor 编辑器 UI 全量编译
|
||||
# OFF:game 构建——预览 pass/atlas/请求池、previewEnvSets、App/editor 源码
|
||||
# 整体不进编译产物(宏裁剪,非运行时判断)
|
||||
option(HELLOVULKAN_EDITOR "Build editor modules (material preview, inspector UI)" ON)
|
||||
|
||||
|
||||
if(MINGW)
|
||||
set(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -Wl,-Bdynamic -lucrt")
|
||||
|
||||
@@ -50,6 +50,15 @@ endif()
|
||||
# ============ Engine ============
|
||||
|
||||
file(GLOB_RECURSE ENGINE_SOURCES CONFIGURE_DEPENDS src/*.cpp)
|
||||
|
||||
# game 构建(HELLOVULKAN_EDITOR=OFF):editor 专属 pass 整 TU 不进产物
|
||||
#(材质球预览 / Editor gizmo——对应头文件只含声明,无符号产出)
|
||||
if(NOT HELLOVULKAN_EDITOR)
|
||||
list(REMOVE_ITEM ENGINE_SOURCES
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Renderer/Passes/MaterialPreviewPass.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Renderer/Passes/EditorGizmoPass.cpp)
|
||||
endif()
|
||||
|
||||
add_library(Engine STATIC ${ENGINE_SOURCES})
|
||||
|
||||
# PhysicsDebugDraw 派生 Jolt::DebugRendererSimple:Jolt 以 -fno-rtti 编译
|
||||
@@ -79,3 +88,9 @@ target_link_libraries(Engine PUBLIC
|
||||
tracy)
|
||||
|
||||
target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
|
||||
|
||||
# editor/game 编译期分离:HELLO_EDITOR=1 时预览子系统 + 编辑器消费端编译
|
||||
#(PUBLIC——App 层 main.cpp 的编辑器装配同样按此宏裁剪)
|
||||
if(HELLOVULKAN_EDITOR)
|
||||
target_compile_definitions(Engine PUBLIC HELLO_EDITOR=1)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,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;
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
//
|
||||
// AnimationAttach 实现:外部动画资产 → 目标模型并入。
|
||||
//
|
||||
// 轨道映射两级:
|
||||
// ① 同名直映——同源拆分资产(角色/动画同一骨架导出)零损耗;
|
||||
// ② Humanoid 槽位重定向——名字不同源的组装资产(角色 FBX +
|
||||
// 动画 FBX 各自制作),经 Unity Avatar 语义的标准人形槽位配对,
|
||||
// 逐 key 烘焙"rest→anim 的增量变换"到目标骨架局部空间:
|
||||
// 平移 out.pos = dstRestPos + (srcPos - srcRestPos)
|
||||
// 旋转 Δ = srcRot * inv(srcRestRot);out.rot = Δ * dstRestRot
|
||||
// (Δ 父空间左乘 = 骨骼自身摆动在两套骨架间等价传递)
|
||||
// 烘焙后 clip 即目标骨架空间数据,评估器零改动。
|
||||
//
|
||||
// AssetManager 是 facade:取模型 + 解析磁盘,纯算法在这里。
|
||||
//
|
||||
|
||||
#include <Engine/Animation/AnimationAttach.h>
|
||||
#include <Engine/Animation/HumanoidRig.h>
|
||||
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
namespace {
|
||||
|
||||
// mat4 → TRS 分解(rest 局部变换;引擎 node 无 shear)
|
||||
struct TRS {
|
||||
glm::vec3 pos{};
|
||||
glm::quat rot{1.0f, 0.0f, 0.0f, 0.0f};
|
||||
glm::vec3 scl{1.0f};
|
||||
};
|
||||
|
||||
TRS Decompose(const glm::mat4& m)
|
||||
{
|
||||
TRS t;
|
||||
t.pos = glm::vec3(m[3]);
|
||||
const glm::vec3 colX(m[0]);
|
||||
const glm::vec3 colY(m[1]);
|
||||
const glm::vec3 colZ(m[2]);
|
||||
t.scl = {glm::length(colX), glm::length(colY),
|
||||
glm::length(colZ)};
|
||||
if (t.scl.x > 1e-8f && t.scl.y > 1e-8f && t.scl.z > 1e-8f) {
|
||||
t.rot = glm::quat_cast(glm::mat3(
|
||||
colX / t.scl.x, colY / t.scl.y, colZ / t.scl.z));
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AttachResult AttachClipsToModel(Model& target, ParsedModel&& src,
|
||||
const std::string& sourcePath)
|
||||
{
|
||||
AttachResult res;
|
||||
// 幂等:该源文件已并入过(自愈重放/重复 attach 直接返回)
|
||||
for (const auto& c : target.GetClips()) {
|
||||
if (c.sourcePath == sourcePath) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
if (src.clips.empty()) {
|
||||
ENGINE_WARN("AttachClipsToModel: no clips in '{}'", sourcePath);
|
||||
return res;
|
||||
}
|
||||
|
||||
const std::vector<ModelNode>& dstNodes = target.GetNodes();
|
||||
const std::vector<ModelNode>& srcNodes = src.nodes;
|
||||
|
||||
// ① 同名直映表
|
||||
std::unordered_map<std::string, u32> nameToIdx;
|
||||
nameToIdx.reserve(dstNodes.size());
|
||||
for (u32 i = 0; i < dstNodes.size(); ++i) {
|
||||
nameToIdx.emplace(dstNodes[i].name, i);
|
||||
}
|
||||
// ② Humanoid 槽位映射表(src nodeIdx → dst nodeIdx)
|
||||
std::unordered_map<u32, u32> retargetMap;
|
||||
std::unordered_map<u32, HumanoidSlot> retargetSlot;
|
||||
{
|
||||
const auto pairs = AutoMapHumanoid(srcNodes, dstNodes);
|
||||
for (const auto& [si, di] : pairs) {
|
||||
retargetMap.emplace(si, di);
|
||||
// 槽位回查(明细行显示用):源骨骼名识别即可
|
||||
Side side;
|
||||
bool ok = false;
|
||||
retargetSlot.emplace(si, MatchHumanoidSlot(
|
||||
srcNodes[si].name, side, ok));
|
||||
}
|
||||
}
|
||||
// 重定向骨架的 rest(逐 nodeIdx 缓存,避免每 key 重分解)
|
||||
std::unordered_map<u32, TRS> dstRestCache;
|
||||
dstRestCache.reserve(retargetMap.size());
|
||||
for (const auto& [si, di] : retargetMap) {
|
||||
dstRestCache.emplace(si, Decompose(dstNodes[di].localTransform));
|
||||
}
|
||||
|
||||
const std::string stem =
|
||||
std::filesystem::path(sourcePath).stem().string();
|
||||
std::vector<AnimationClip> extra;
|
||||
extra.reserve(src.clips.size());
|
||||
for (auto& clip : src.clips) {
|
||||
AnimationClip out;
|
||||
out.name = clip.name;
|
||||
out.duration = clip.duration;
|
||||
out.tps = clip.tps;
|
||||
out.sourcePath = sourcePath;
|
||||
// 同名冲突(内嵌 clip 或其它源):源文件名循环加前缀直到
|
||||
// 唯一。单轮前缀在"前缀名也撞"(如两次 attach 同名资产)时
|
||||
// 仍会产出重复名——Controller/脚本按 clip 名引用,重名即歧义
|
||||
{
|
||||
const auto collides = [&](const std::string& n) {
|
||||
for (const auto& c : target.GetClips()) {
|
||||
if (c.name == n) return true;
|
||||
}
|
||||
for (const auto& c : extra) {
|
||||
if (c.name == n) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
while (collides(out.name)) {
|
||||
out.name = stem + "_" + out.name;
|
||||
}
|
||||
}
|
||||
for (auto& ch : clip.channels) {
|
||||
if (ch.positions.empty() && ch.rotations.empty() &&
|
||||
ch.scales.empty()) {
|
||||
continue; // 空轨道无用
|
||||
}
|
||||
if (ch.nodeIdx >= srcNodes.size()) {
|
||||
++res.channelsSkipped;
|
||||
continue;
|
||||
}
|
||||
const std::string& boneName = srcNodes[ch.nodeIdx].name;
|
||||
|
||||
AnimChannel remapped;
|
||||
if (auto it = nameToIdx.find(boneName);
|
||||
it != nameToIdx.end()) {
|
||||
// ① 同名直搬
|
||||
remapped.nodeIdx = it->second;
|
||||
remapped.positions = std::move(ch.positions);
|
||||
remapped.rotations = std::move(ch.rotations);
|
||||
remapped.scales = std::move(ch.scales);
|
||||
out.channels.push_back(std::move(remapped));
|
||||
++res.channelsMatched;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ② 槽位重定向:增量烘焙到目标骨架局部空间。
|
||||
// 平移增量按骨长比缩放(|dstRest| / |srcRest|)——
|
||||
// 两资产单位/比例不同源(FBX 常为 cm、glTF 为 m)时
|
||||
// 不缩放会把模型拽飞;rest≈0 的根骨骼比例取 1
|
||||
const auto mapIt = retargetMap.find(ch.nodeIdx);
|
||||
if (mapIt == retargetMap.end()) {
|
||||
++res.channelsSkipped; // 两级都落不上(手指/道具等)
|
||||
continue;
|
||||
}
|
||||
const TRS srcRest = Decompose(
|
||||
srcNodes[ch.nodeIdx].localTransform);
|
||||
const TRS& dstRest = dstRestCache[ch.nodeIdx];
|
||||
remapped.nodeIdx = mapIt->second;
|
||||
const f32 srcLen = glm::length(srcRest.pos);
|
||||
const f32 ratio =
|
||||
(srcLen > 1e-6f) ? (glm::length(dstRest.pos) / srcLen)
|
||||
: 1.0f;
|
||||
|
||||
remapped.positions.reserve(ch.positions.size());
|
||||
for (const auto& k : ch.positions) {
|
||||
remapped.positions.push_back(
|
||||
{k.time, dstRest.pos +
|
||||
(k.value - srcRest.pos) * ratio});
|
||||
}
|
||||
remapped.rotations.reserve(ch.rotations.size());
|
||||
for (const auto& k : ch.rotations) {
|
||||
const glm::quat delta =
|
||||
k.value * glm::inverse(srcRest.rot);
|
||||
remapped.rotations.push_back(
|
||||
{k.time, delta * dstRest.rot});
|
||||
}
|
||||
remapped.scales = std::move(ch.scales);
|
||||
out.channels.push_back(std::move(remapped));
|
||||
++res.channelsRetargeted;
|
||||
|
||||
// 明细行(同骨骼多 clip 去重)
|
||||
const bool newRow = std::none_of(
|
||||
res.retargetRows.begin(), res.retargetRows.end(),
|
||||
[&](const RetargetRow& r)
|
||||
{ return r.srcName == boneName; });
|
||||
if (newRow) {
|
||||
res.retargetRows.push_back(
|
||||
{HumanoidSlotName(retargetSlot[ch.nodeIdx]),
|
||||
boneName, dstNodes[mapIt->second].name});
|
||||
}
|
||||
}
|
||||
// 全部轨道落不上 = 无可驱动骨骼,clip 不入表
|
||||
if (out.channels.empty()) {
|
||||
ENGINE_WARN("AttachClipsToModel: clip '{}' has no matching "
|
||||
"bones in target (skipped)",
|
||||
clip.name);
|
||||
continue;
|
||||
}
|
||||
extra.push_back(std::move(out));
|
||||
++res.clipsAdded;
|
||||
}
|
||||
if (!extra.empty()) {
|
||||
target.AppendClips(std::move(extra));
|
||||
}
|
||||
ENGINE_INFO("AttachClipsToModel: '{}' -> {} clips "
|
||||
"({} named, {} retargeted, {} skipped)",
|
||||
sourcePath, res.clipsAdded, res.channelsMatched,
|
||||
res.channelsRetargeted, res.channelsSkipped);
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace Engine::Animation
|
||||
@@ -0,0 +1,390 @@
|
||||
//
|
||||
// AnimationController 实现:.animctrl YAML 解析。
|
||||
//
|
||||
// 纯资产解析(无运行时状态):字符串条件在这里一次性编译成数值比较
|
||||
// 结构(TransitionCond),System 每帧只做查表比较,零字符串处理。
|
||||
//
|
||||
|
||||
#include <Engine/Animation/AnimationController.h>
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
// 2D 方向混合权重:原点层 1-|v| + 覆盖扇区角度插值 ×min(|v|,1)。
|
||||
// 数学与原 System 内联版一致,抽公共函数后编辑器方向盘可视化
|
||||
// 与运行时采样永不漂移。
|
||||
// kMinBlendWeight:方向层权重低于阈值即不输出——中心附近每个
|
||||
// 方向层都有"角度份额×|v|"的微量残留,不滤的话"读出显示 0.00
|
||||
// 却还在影响姿态"(四舍五入假象)+ 无效微量混合。原点层不受
|
||||
// 此限(它是中心默认动作,v 小时权重接近 1)
|
||||
constexpr f32 kMinBlendWeight = 0.01f;
|
||||
|
||||
void EvaluateBlendTree2D(const BlendTree& tree, f32 x, f32 z,
|
||||
std::vector<std::pair<u32, f32>>& out)
|
||||
{
|
||||
out.clear();
|
||||
const auto& ch = tree.children;
|
||||
const f32 r = glm::min(glm::length(glm::vec2(x, z)), 1.0f);
|
||||
// 原点子节点:权重 = 1-|v| 随模长连续衰减(Unity 2D 语义)——
|
||||
// 待机到方向动作平滑过渡。v=0 时全量,拖到边缘衰减为 0;
|
||||
// 任何实现里原点层都不允许"v≠0 即消失"(那样中心拖动会瞬切)
|
||||
for (u32 i = 0; i < ch.size(); ++i) {
|
||||
if (glm::length(ch[i].pos) < 1e-4f) {
|
||||
if (1.0f - r > 1e-4f) out.push_back({i, 1.0f - r});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (r <= 1e-4f || ch.empty()) return;
|
||||
// 覆盖 v 角度的相邻扇区(角度归一 [0,2π),wrap 补全;方向子
|
||||
// ≤16 个,栈数组零堆分配)
|
||||
constexpr f32 kTwoPi = 6.2831853f;
|
||||
const f32 theta = glm::mod(glm::atan(z, x), kTwoPi);
|
||||
u32 idx[16];
|
||||
f32 ang[16];
|
||||
u32 nd = 0;
|
||||
for (u32 i = 0; i < ch.size() && nd < 16; ++i) {
|
||||
if (glm::length(ch[i].pos) < 1e-4f) continue;
|
||||
idx[nd] = i;
|
||||
ang[nd] = glm::mod(glm::atan(ch[i].pos.y, ch[i].pos.x), kTwoPi);
|
||||
++nd;
|
||||
}
|
||||
for (u32 i = 0; i < nd; ++i) {
|
||||
const u32 j = (i + 1) % nd;
|
||||
const f32 ai = ang[i];
|
||||
const f32 aj = (j == 0) ? ang[0] + kTwoPi : ang[j];
|
||||
if (theta >= ai && theta <= aj) {
|
||||
const f32 span = aj - ai;
|
||||
const f32 t = span > 1e-6f ? (theta - ai) / span : 0.0f;
|
||||
const f32 wi = (1.0f - t) * r;
|
||||
const f32 wj = t * r;
|
||||
if (wi >= kMinBlendWeight) out.push_back({idx[i], wi});
|
||||
if (wj >= kMinBlendWeight) out.push_back({idx[j], wj});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// "Speed > 0.1" / "IsJump == true" / ""(恒真)→ 数值比较结构。
|
||||
// 未知参数/坏格式 = invalid(永不触发 + WARN,资产错误不崩运行时)
|
||||
TransitionCond AnimationController::ParseCond(const std::string& s)
|
||||
{
|
||||
TransitionCond c;
|
||||
if (s.empty()) {
|
||||
c.always = true;
|
||||
c.valid = true;
|
||||
return c;
|
||||
}
|
||||
std::istringstream ss(s);
|
||||
std::string param, op, operand;
|
||||
ss >> param >> op >> operand;
|
||||
c.param = param;
|
||||
if (param.empty() || op.empty() || operand.empty()) {
|
||||
ENGINE_WARN("AnimationController: bad cond '{}' (want 'P op X')",
|
||||
s);
|
||||
return c;
|
||||
}
|
||||
if (op == ">") c.op = TransitionCond::Op::Gt;
|
||||
else if (op == "<") c.op = TransitionCond::Op::Lt;
|
||||
else if (op == ">=") c.op = TransitionCond::Op::Ge;
|
||||
else if (op == "<=") c.op = TransitionCond::Op::Le;
|
||||
else if (op == "==") c.op = TransitionCond::Op::Eq;
|
||||
else if (op == "!=") c.op = TransitionCond::Op::Ne;
|
||||
else {
|
||||
ENGINE_WARN("AnimationController: unknown op '{}' in '{}'",
|
||||
op, s);
|
||||
return c;
|
||||
}
|
||||
if (operand == "true") {
|
||||
c.isBool = true;
|
||||
c.boolOperand = true;
|
||||
} else if (operand == "false") {
|
||||
c.isBool = true;
|
||||
c.boolOperand = false;
|
||||
} else {
|
||||
try {
|
||||
size_t consumed = 0;
|
||||
c.numOperand = std::stof(operand, &consumed);
|
||||
if (consumed != operand.size()) throw std::invalid_argument("");
|
||||
} catch (const std::exception&) {
|
||||
ENGINE_WARN("AnimationController: bad operand '{}' in '{}'",
|
||||
operand, s);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
c.valid = true;
|
||||
return c;
|
||||
}
|
||||
|
||||
Scope<AnimationController>
|
||||
AnimationController::Load(const std::string& path)
|
||||
{
|
||||
YAML::Node root;
|
||||
try {
|
||||
root = YAML::LoadFile(path);
|
||||
} catch (const std::exception& e) {
|
||||
ENGINE_ERROR("AnimationController: failed to parse '{}': {}",
|
||||
path, e.what());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto ctrl = CreateScope<AnimationController>();
|
||||
ctrl->m_Path = path;
|
||||
|
||||
// ---- Params(参数声明;类型缺省 float)----
|
||||
if (const YAML::Node params = root["Params"]) {
|
||||
for (const auto& p : params) {
|
||||
ControllerParam d;
|
||||
d.name = p["name"].as<std::string>("");
|
||||
const std::string type =
|
||||
p["type"].as<std::string>("float");
|
||||
if (type == "bool") d.type = ControllerParam::Type::Bool;
|
||||
else if (type == "trigger")
|
||||
d.type = ControllerParam::Type::Trigger;
|
||||
if (d.type == ControllerParam::Type::Bool)
|
||||
d.defBool = p["default"].as<bool>(false);
|
||||
else
|
||||
d.defFloat = p["default"].as<f32>(0.0f);
|
||||
if (!d.name.empty()) {
|
||||
ctrl->m_Params.push_back(std::move(d));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- States(map:state 名 → 定义;文件序即声明序)----
|
||||
if (const YAML::Node states = root["States"]) {
|
||||
for (const auto& it : states) {
|
||||
ControllerState st;
|
||||
st.name = it.first.as<std::string>("");
|
||||
const YAML::Node sn = it.second;
|
||||
if (st.name.empty()) continue;
|
||||
if (const YAML::Node clip = sn["clip"]) {
|
||||
st.tree.type = BlendTree::Type::Single;
|
||||
st.tree.clip = clip.as<std::string>("");
|
||||
}
|
||||
if (const YAML::Node bt = sn["blendTree"]) {
|
||||
const std::string btType =
|
||||
bt["type"].as<std::string>("2DDirectional");
|
||||
st.tree.type =
|
||||
(btType == "1D" || btType == "1DDirectional")
|
||||
? BlendTree::Type::Directional1D
|
||||
: BlendTree::Type::Directional2D;
|
||||
st.tree.xParam = bt["x"].as<std::string>("");
|
||||
if (const YAML::Node z = bt["z"]) {
|
||||
st.tree.zParam = z.as<std::string>("");
|
||||
}
|
||||
if (const YAML::Node children = bt["children"]) {
|
||||
for (const auto& c : children) {
|
||||
BlendTreeChild ch;
|
||||
ch.clip = c["clip"].as<std::string>("");
|
||||
if (const YAML::Node at = c["at"]) {
|
||||
if (at.IsSequence() && at.size() >= 1) {
|
||||
ch.pos.x = at[0].as<f32>(0.0f);
|
||||
if (at.size() >= 2) {
|
||||
ch.pos.y = at[1].as<f32>(0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ch.clip.empty()) {
|
||||
st.tree.children.push_back(std::move(ch));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
st.speed = sn["speed"].as<f32>(1.0f);
|
||||
st.loop = sn["loop"].as<bool>(true);
|
||||
if (const YAML::Node te = sn["transitionsOnEnd"]) {
|
||||
for (const auto& t : te) {
|
||||
st.transitionsOnEnd.push_back(
|
||||
t.as<std::string>(""));
|
||||
}
|
||||
}
|
||||
// BlendTree 子节点排序(Load 期一次,System 每帧直接
|
||||
// 扫描):1D 按阈值升序;2D 按角度升序(原点子节点
|
||||
// 角度未定义留在原位——扇区查找时按 |pos|<ε 排除)
|
||||
if (st.tree.type == BlendTree::Type::Directional1D) {
|
||||
std::sort(st.tree.children.begin(),
|
||||
st.tree.children.end(),
|
||||
[](const BlendTreeChild& a,
|
||||
const BlendTreeChild& b)
|
||||
{ return a.pos.x < b.pos.x; });
|
||||
} else if (st.tree.type ==
|
||||
BlendTree::Type::Directional2D) {
|
||||
std::sort(st.tree.children.begin(),
|
||||
st.tree.children.end(),
|
||||
[](const BlendTreeChild& a,
|
||||
const BlendTreeChild& b) {
|
||||
return std::atan2(a.pos.y, a.pos.x) <
|
||||
std::atan2(b.pos.y, b.pos.x);
|
||||
});
|
||||
}
|
||||
ctrl->m_StateOrder.push_back(st.name);
|
||||
ctrl->m_States.emplace(st.name, std::move(st));
|
||||
}
|
||||
}
|
||||
if (ctrl->m_States.empty()) {
|
||||
ENGINE_ERROR("AnimationController: '{}' has no states", path);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ---- Entry(显式键优先,缺省 = 声明序首个)----
|
||||
if (const YAML::Node entry = root["Entry"]) {
|
||||
ctrl->m_Entry = entry.as<std::string>("");
|
||||
}
|
||||
if (ctrl->m_Entry.empty() ||
|
||||
!ctrl->m_States.count(ctrl->m_Entry)) {
|
||||
if (!ctrl->m_Entry.empty()) {
|
||||
ENGINE_WARN("AnimationController: entry '{}' not found in "
|
||||
"'{}', falling back to first state",
|
||||
ctrl->m_Entry, path);
|
||||
}
|
||||
ctrl->m_Entry = ctrl->m_StateOrder.front();
|
||||
}
|
||||
|
||||
// ---- Transitions(目标不存在的行丢弃 + WARN)----
|
||||
if (const YAML::Node trans = root["Transitions"]) {
|
||||
for (const auto& t : trans) {
|
||||
Transition tr;
|
||||
tr.from = t["from"].as<std::string>("*");
|
||||
tr.to = t["to"].as<std::string>("");
|
||||
tr.fade = t["fade"].as<f32>(0.25f);
|
||||
tr.hasExitTime = t["exitTime"].as<bool>(false);
|
||||
tr.cond = ParseCond(t["cond"].as<std::string>(""));
|
||||
if (tr.to.empty() || !ctrl->m_States.count(tr.to)) {
|
||||
ENGINE_WARN("AnimationController: transition to "
|
||||
"unknown state '{}' in '{}'",
|
||||
tr.to, path);
|
||||
continue;
|
||||
}
|
||||
ctrl->m_Transitions.push_back(std::move(tr));
|
||||
}
|
||||
}
|
||||
|
||||
ENGINE_INFO("AnimationController: loaded '{}' ({} states, "
|
||||
"{} params, {} transitions)",
|
||||
path, ctrl->m_States.size(), ctrl->m_Params.size(),
|
||||
ctrl->m_Transitions.size());
|
||||
return ctrl;
|
||||
}
|
||||
|
||||
// Save:与 Load 对齐的 YAML 反序列化(f32 紧凑格式 %.4g;clip/状态
|
||||
// 名加引号——mixamorig: 前缀等含冒号名不破键值解析)
|
||||
static std::string FmtF(f32 v)
|
||||
{
|
||||
char b[32];
|
||||
snprintf(b, sizeof(b), "%.4g", static_cast<double>(v));
|
||||
return b;
|
||||
}
|
||||
|
||||
std::string AnimationController::CondToString(const TransitionCond& c)
|
||||
{
|
||||
const char* op = "=";
|
||||
switch (c.op) {
|
||||
case TransitionCond::Op::Gt: op = ">"; break;
|
||||
case TransitionCond::Op::Lt: op = "<"; break;
|
||||
case TransitionCond::Op::Ge: op = ">="; break;
|
||||
case TransitionCond::Op::Le: op = "<="; break;
|
||||
case TransitionCond::Op::Eq: op = "=="; break;
|
||||
case TransitionCond::Op::Ne: op = "!="; break;
|
||||
}
|
||||
const std::string operand =
|
||||
c.isBool ? (c.boolOperand ? "true" : "false")
|
||||
: FmtF(c.numOperand);
|
||||
return c.param + " " + op + " " + operand;
|
||||
}
|
||||
|
||||
bool AnimationController::Save(const std::string& path) const
|
||||
{
|
||||
std::string y =
|
||||
"# 动画控制器(.animctrl)—— ControllerEditorPanel 保存产物\n"
|
||||
"# 状态/参数/过渡全由本文件声明,引擎零硬编码\n";
|
||||
if (!m_Entry.empty()) {
|
||||
y += "Entry: \"" + m_Entry + "\"\n";
|
||||
}
|
||||
y += "Params:\n";
|
||||
for (const auto& p : m_Params) {
|
||||
y += " - {name: \"" + p.name + "\", type: ";
|
||||
if (p.type == ControllerParam::Type::Float) {
|
||||
y += "float, default: " + FmtF(p.defFloat) + "}";
|
||||
} else if (p.type == ControllerParam::Type::Bool) {
|
||||
y += std::string("bool, default: ") +
|
||||
(p.defBool ? "true" : "false") + "}";
|
||||
} else {
|
||||
y += "trigger}";
|
||||
}
|
||||
y += "\n";
|
||||
}
|
||||
y += "States:\n";
|
||||
for (const auto& n : m_StateOrder) {
|
||||
const auto it = m_States.find(n);
|
||||
if (it == m_States.end()) continue;
|
||||
const auto& s = it->second;
|
||||
y += " \"" + n + "\":\n";
|
||||
y += " speed: " + FmtF(s.speed) + "\n";
|
||||
y += std::string(" loop: ") + (s.loop ? "true" : "false") +
|
||||
"\n";
|
||||
if (!s.transitionsOnEnd.empty()) {
|
||||
y += " transitionsOnEnd: [";
|
||||
for (size_t i = 0; i < s.transitionsOnEnd.size(); ++i) {
|
||||
// 状态名加引号(含冒号/逗号的名不破 flow 序列解析,
|
||||
// 同 States 键与 Transitions from/to 惯例)
|
||||
y += (i ? ", \"" : "\"") + s.transitionsOnEnd[i] + "\"";
|
||||
}
|
||||
y += "]\n";
|
||||
}
|
||||
const auto& t = s.tree;
|
||||
if (t.type == BlendTree::Type::Single) {
|
||||
y += " clip: \"" + t.clip + "\"\n";
|
||||
} else {
|
||||
y += " blendTree:\n type: ";
|
||||
y += (t.type == BlendTree::Type::Directional1D)
|
||||
? "1DDirectional\n"
|
||||
: "2DDirectional\n";
|
||||
y += " x: \"" + t.xParam + "\"\n";
|
||||
if (t.type == BlendTree::Type::Directional2D) {
|
||||
y += " z: \"" + t.zParam + "\"\n";
|
||||
}
|
||||
y += " children:\n";
|
||||
for (const auto& c : t.children) {
|
||||
y += " - {clip: \"" + c.clip + "\", at: [" +
|
||||
FmtF(c.pos.x);
|
||||
if (t.type == BlendTree::Type::Directional2D) {
|
||||
y += ", " + FmtF(c.pos.y);
|
||||
}
|
||||
y += "]}\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
y += "Transitions:\n";
|
||||
for (const auto& t : m_Transitions) {
|
||||
y += " - {from: \"" + t.from + "\", to: \"" + t.to + "\"";
|
||||
if (t.hasExitTime) {
|
||||
y += ", exitTime: true";
|
||||
}
|
||||
if (!t.cond.always && t.cond.valid) {
|
||||
y += ", cond: \"" + CondToString(t.cond) + "\"";
|
||||
}
|
||||
y += ", fade: " + FmtF(t.fade) + "}\n";
|
||||
}
|
||||
std::ofstream f(path, std::ios::binary | std::ios::trunc);
|
||||
if (!f) {
|
||||
ENGINE_ERROR("AnimationController: cannot write '{}'", path);
|
||||
return false;
|
||||
}
|
||||
f.write(y.data(),
|
||||
static_cast<std::streamsize>(y.size()));
|
||||
ENGINE_INFO("AnimationController: saved '{}' ({} states, "
|
||||
"{} transitions)",
|
||||
path, m_States.size(), m_Transitions.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Engine::Animation
|
||||
@@ -0,0 +1,226 @@
|
||||
//
|
||||
// AnimationEvaluator 实现:采样内核自 SceneRenderer 原样迁移(数学零
|
||||
// 改动,行为不变),补 ComputeRestPose 供骨骼可视化用。
|
||||
//
|
||||
|
||||
#include <Engine/Animation/AnimationEvaluator.h>
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
namespace {
|
||||
|
||||
// 静止 local TRS 分解(列主序 SRT 矩阵;假设无负缩放/剪切)。
|
||||
// 被动画轨道触碰的节点用关键帧重建,未触碰的保持原矩阵——
|
||||
// FBX PreRotation 折在 localTransform 里,拆 TRS 重建会丢
|
||||
void DecomposeTRS(const glm::mat4& m, glm::vec3& pos, glm::quat& rot,
|
||||
glm::vec3& scl)
|
||||
{
|
||||
pos = glm::vec3(m[3]);
|
||||
scl = {glm::length(glm::vec3(m[0])),
|
||||
glm::length(glm::vec3(m[1])),
|
||||
glm::length(glm::vec3(m[2]))};
|
||||
const glm::mat3 r(glm::vec3(m[0]) / scl.x,
|
||||
glm::vec3(m[1]) / scl.y,
|
||||
glm::vec3(m[2]) / scl.z);
|
||||
rot = glm::quat_cast(r);
|
||||
}
|
||||
|
||||
// 关键帧采样:前后键插值(位置/缩放 lerp,旋转 slerp)。
|
||||
// key 数通常几十个,线性扫描对 cache 更友好
|
||||
glm::vec3 SampleKeys(const std::vector<PosKey>& keys, f32 t,
|
||||
const glm::vec3& rest)
|
||||
{
|
||||
if (keys.empty()) return rest;
|
||||
if (t <= keys.front().time) return keys.front().value;
|
||||
for (size_t i = 0; i + 1 < keys.size(); ++i) {
|
||||
if (t < keys[i + 1].time) {
|
||||
const f32 a = keys[i].time, b = keys[i + 1].time;
|
||||
return glm::mix(keys[i].value, keys[i + 1].value,
|
||||
b > a ? (t - a) / (b - a) : 0.0f);
|
||||
}
|
||||
}
|
||||
return keys.back().value;
|
||||
}
|
||||
|
||||
glm::vec3 SampleKeys(const std::vector<SclKey>& keys, f32 t,
|
||||
const glm::vec3& rest)
|
||||
{
|
||||
if (keys.empty()) return rest;
|
||||
if (t <= keys.front().time) return keys.front().value;
|
||||
for (size_t i = 0; i + 1 < keys.size(); ++i) {
|
||||
if (t < keys[i + 1].time) {
|
||||
const f32 a = keys[i].time, b = keys[i + 1].time;
|
||||
return glm::mix(keys[i].value, keys[i + 1].value,
|
||||
b > a ? (t - a) / (b - a) : 0.0f);
|
||||
}
|
||||
}
|
||||
return keys.back().value;
|
||||
}
|
||||
|
||||
glm::quat SampleKeys(const std::vector<RotKey>& keys, f32 t,
|
||||
const glm::quat& rest)
|
||||
{
|
||||
if (keys.empty()) return rest;
|
||||
if (t <= keys.front().time) return keys.front().value;
|
||||
for (size_t i = 0; i + 1 < keys.size(); ++i) {
|
||||
if (t < keys[i + 1].time) {
|
||||
const f32 a = keys[i].time, b = keys[i + 1].time;
|
||||
return glm::slerp(keys[i].value, keys[i + 1].value,
|
||||
b > a ? (t - a) / (b - a) : 0.0f);
|
||||
}
|
||||
}
|
||||
return keys.back().value;
|
||||
}
|
||||
|
||||
// 单 clip 在 time 的全节点局部 TRS(无轨道分量 = 静止分解值)
|
||||
void SampleLocalPose(const Model& model, const AnimationClip& clip,
|
||||
f32 time, LocalPose& out)
|
||||
{
|
||||
const auto& nodes = model.GetNodes();
|
||||
const size_t n = nodes.size();
|
||||
out.pos.resize(n);
|
||||
out.rot.resize(n);
|
||||
out.scl.resize(n);
|
||||
out.touched.assign(n, 0);
|
||||
for (u32 i = 0; i < n; ++i) {
|
||||
DecomposeTRS(nodes[i].localTransform, out.pos[i], out.rot[i],
|
||||
out.scl[i]);
|
||||
}
|
||||
if (clip.duration <= 0.0f) return;
|
||||
const f32 t = glm::mod(time, clip.duration);
|
||||
for (const auto& ch : clip.channels) {
|
||||
if (ch.nodeIdx >= n) continue;
|
||||
out.touched[ch.nodeIdx] = 1;
|
||||
if (!ch.positions.empty())
|
||||
out.pos[ch.nodeIdx] =
|
||||
SampleKeys(ch.positions, t, out.pos[ch.nodeIdx]);
|
||||
if (!ch.rotations.empty())
|
||||
out.rot[ch.nodeIdx] =
|
||||
SampleKeys(ch.rotations, t, out.rot[ch.nodeIdx]);
|
||||
if (!ch.scales.empty())
|
||||
out.scl[ch.nodeIdx] =
|
||||
SampleKeys(ch.scales, t, out.scl[ch.nodeIdx]);
|
||||
}
|
||||
}
|
||||
|
||||
// 局部 TRS → 全节点 global 矩阵(层级累乘;父序 DFS 保证父先算)
|
||||
void ComposeGlobals(const Model& model, const LocalPose& pose,
|
||||
std::vector<glm::mat4>& globals)
|
||||
{
|
||||
const auto& nodes = model.GetNodes();
|
||||
globals.resize(nodes.size());
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
const glm::mat4 local = pose.touched[i]
|
||||
? glm::translate(glm::mat4(1.0f), pose.pos[i]) *
|
||||
glm::mat4_cast(pose.rot[i]) *
|
||||
glm::scale(glm::mat4(1.0f), pose.scl[i])
|
||||
: nodes[i].localTransform;
|
||||
globals[i] = nodes[i].parent >= 0
|
||||
? globals[static_cast<u32>(nodes[i].parent)] * local
|
||||
: local;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AnimationEvaluator::Evaluate(const Model& model,
|
||||
const AnimationClip& cur, f32 curTime,
|
||||
const CrossfadeState& xf,
|
||||
std::vector<glm::mat4>& outGlobals)
|
||||
{
|
||||
// crossfade 双层 = N 层混合的特例(Direct 模式与 Controller
|
||||
// 过渡共用同一套加权数学)。src 层权重 = 1 - wCur 与历史
|
||||
// mix(src, cur, wCur) 语义等价
|
||||
PoseLayer layers[2];
|
||||
layers[0] = {&cur, curTime, 1.0f};
|
||||
u32 count = 1;
|
||||
if (xf.src != nullptr) {
|
||||
layers[1] = {xf.src, xf.srcTime, 1.0f - xf.wCur};
|
||||
count = 2;
|
||||
}
|
||||
EvaluateLayers(model, layers, count, outGlobals);
|
||||
}
|
||||
|
||||
void AnimationEvaluator::EvaluateLayers(const Model& model,
|
||||
const PoseLayer* layers,
|
||||
u32 count,
|
||||
std::vector<glm::mat4>& outGlobals)
|
||||
{
|
||||
// ---- 逐层采样(容量跨帧复用;nullptr/零权重层跳过)----
|
||||
if (m_Layers.size() < count) {
|
||||
m_Layers.resize(count); // 只扩不缩,采样循环内元素地址稳定
|
||||
}
|
||||
m_Sampled.clear();
|
||||
for (u32 i = 0; i < count; ++i) {
|
||||
if (layers[i].clip == nullptr || layers[i].weight <= 0.0f) {
|
||||
continue;
|
||||
}
|
||||
SampleLocalPose(model, *layers[i].clip, layers[i].time,
|
||||
m_Layers[m_Sampled.size()]);
|
||||
m_Sampled.push_back({&m_Layers[m_Sampled.size()],
|
||||
layers[i].weight});
|
||||
}
|
||||
|
||||
if (m_Sampled.empty()) {
|
||||
// 无有效层 = rest(m_A untouched = localTransform 原样)
|
||||
const size_t n = model.GetNodes().size();
|
||||
m_A.pos.resize(n);
|
||||
m_A.rot.resize(n);
|
||||
m_A.scl.resize(n);
|
||||
m_A.touched.assign(n, 0);
|
||||
ComposeGlobals(model, m_A, outGlobals);
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- 加权归一化混合进 m_A:渐进混合(基层 = 首个有效层,
|
||||
// 逐层以 wi/(acc+wi) 混入;对线性量等价 Σwi·xi/Σwi,
|
||||
// 对旋转是顺序稳定的渐进 slerp——层级混合的通行做法)----
|
||||
{
|
||||
const LocalPose& base = *m_Sampled[0].pose;
|
||||
const size_t n = base.pos.size();
|
||||
m_A.pos.resize(n);
|
||||
m_A.rot.resize(n);
|
||||
m_A.scl.resize(n);
|
||||
m_A.touched.resize(n);
|
||||
for (size_t k = 0; k < n; ++k) {
|
||||
m_A.pos[k] = base.pos[k];
|
||||
m_A.rot[k] = base.rot[k];
|
||||
m_A.scl[k] = base.scl[k];
|
||||
m_A.touched[k] = base.touched[k];
|
||||
}
|
||||
f32 acc = m_Sampled[0].weight;
|
||||
for (size_t l = 1; l < m_Sampled.size(); ++l) {
|
||||
const LocalPose& src = *m_Sampled[l].pose;
|
||||
const f32 w =
|
||||
m_Sampled[l].weight / (acc + m_Sampled[l].weight);
|
||||
for (size_t k = 0; k < n; ++k) {
|
||||
// touched = 各层轨道并集(重建集合只扩大)
|
||||
m_A.touched[k] =
|
||||
static_cast<u8>(m_A.touched[k] | src.touched[k]);
|
||||
m_A.pos[k] = glm::mix(m_A.pos[k], src.pos[k], w);
|
||||
m_A.scl[k] = glm::mix(m_A.scl[k], src.scl[k], w);
|
||||
// 旋转必须 slerp(lerp 单位四元数会走样)
|
||||
m_A.rot[k] = glm::slerp(m_A.rot[k], src.rot[k], w);
|
||||
}
|
||||
acc += m_Sampled[l].weight;
|
||||
}
|
||||
}
|
||||
ComposeGlobals(model, m_A, outGlobals);
|
||||
}
|
||||
|
||||
void AnimationEvaluator::ComputeRestPose(
|
||||
const Model& model, std::vector<glm::mat4>& outGlobals)
|
||||
{
|
||||
const auto& nodes = model.GetNodes();
|
||||
outGlobals.resize(nodes.size());
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
outGlobals[i] = nodes[i].parent >= 0
|
||||
? outGlobals[static_cast<u32>(nodes[i].parent)] *
|
||||
nodes[i].localTransform
|
||||
: nodes[i].localTransform;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Engine::Animation
|
||||
@@ -0,0 +1,573 @@
|
||||
//
|
||||
// AnimationSystem 实现:调度逻辑自 SceneRenderer::BuildFrame 第 2.5 节
|
||||
// 原样迁移(播放头/crossfade/palette 分配数学零改动),骨骼可视化段为
|
||||
// 新增(编辑形态专属)。
|
||||
//
|
||||
|
||||
#include <Engine/Animation/AnimationSystem.h>
|
||||
|
||||
#include <Engine/Animation/AnimationEvaluator.h>
|
||||
#include <Engine/Animation/AnimationController.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
void AnimationSystem::Update(ECS::Scene& scene, Assets::AssetManager& assets,
|
||||
f32 dt, FrameSceneData& frame)
|
||||
{
|
||||
// palette 槽 0 固定 identity:非蒙皮静态模型直通(蒙皮但无
|
||||
// 动画采样的实体由下方 rest-pose 兜底覆盖,见第二个 Each)
|
||||
frame.skinPalettes.clear();
|
||||
frame.skinPalettes.push_back(glm::mat4(1.0f));
|
||||
frame.entitySkinBases.clear();
|
||||
frame.skinPaletteTruncated = false;
|
||||
#ifdef HELLO_EDITOR
|
||||
frame.boneWireVerts.clear();
|
||||
frame.boneJoints.clear();
|
||||
m_SelHasPose = false;
|
||||
#endif
|
||||
|
||||
// ---- palette bump 分配(Direct/Controller 共用):按实体,
|
||||
// 不按模型去重——同模型两实体可不同播放状态。预算打满
|
||||
// → 该实体退回绑定姿势(rest 兜底接手)----
|
||||
auto PushPalette = [&](ECS::Entity entity, const Model* model) {
|
||||
const auto& bones = model->GetBones();
|
||||
const u32 base = static_cast<u32>(frame.skinPalettes.size());
|
||||
if (base + bones.size() > kMaxSkinMatrices) {
|
||||
frame.skinPaletteTruncated = true;
|
||||
if (!m_TruncWarned) {
|
||||
m_TruncWarned = true;
|
||||
ENGINE_WARN("AnimationSystem: skin palette full ({}), "
|
||||
"animated entities fall back to bind pose",
|
||||
kMaxSkinMatrices);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const auto& b : bones) {
|
||||
const u32 ni = b.nodeIdx < m_Globals.size()
|
||||
? b.nodeIdx : 0;
|
||||
frame.skinPalettes.push_back(m_Globals[ni] * b.invBind);
|
||||
}
|
||||
const u32 entKey = static_cast<u32>(entt::to_integral(entity));
|
||||
frame.entitySkinBases[entKey] = base;
|
||||
#ifdef HELLO_EDITOR
|
||||
// 选中实体 = 暂存当帧姿态(骨骼可视化与渲染同源)
|
||||
if (frame.selectedEntityValid &&
|
||||
frame.selectedEntityBits == entKey) {
|
||||
m_SelGlobals = m_Globals;
|
||||
m_SelHasPose = true;
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
// ---- 段 1:Direct 模式(单 clip 直控 + crossfade)----
|
||||
scene.Each<ECS::MeshRendererComponent, ECS::AnimationComponent>(
|
||||
[&](ECS::Entity entity, ECS::MeshRendererComponent& mr,
|
||||
ECS::AnimationComponent& anim) {
|
||||
if (anim.mode != ECS::AnimationComponent::AnimMode::Direct)
|
||||
return; // Controller 实体走段 2
|
||||
if (!anim.active) return; // 组件禁用 = 暂停采样(播放头不动)
|
||||
if (!mr.model.Valid()) return; // 未指派/手清 = 无效句柄(Get 对无效 FATAL)
|
||||
const Model* model = assets.GetModel(mr.model);
|
||||
if (!model || !model->HasAnimations()) return;
|
||||
const auto& clips = model->GetClips();
|
||||
|
||||
// clip 下标合法性(模型资产可能已换)
|
||||
if (anim.clipIndex < 0 ||
|
||||
anim.clipIndex >= static_cast<int>(clips.size())) {
|
||||
anim.clipIndex = 0;
|
||||
anim.fadeFrom = -1;
|
||||
if (clips.empty()) return;
|
||||
}
|
||||
if (anim.fadeFrom >= static_cast<int>(clips.size())) {
|
||||
anim.fadeFrom = -1;
|
||||
}
|
||||
|
||||
// ---- 播放头推进(wrap/clamp)----
|
||||
if (anim.playing && anim.speed != 0.0f) {
|
||||
const f32 d = dt * anim.speed;
|
||||
anim.time += d;
|
||||
if (anim.fadeFrom >= 0) anim.fadeFromTime += d;
|
||||
}
|
||||
const auto wrapTime = [&](f32 t, const AnimationClip& c) {
|
||||
if (c.duration <= 0.0f) return 0.0f;
|
||||
return anim.loop ? glm::mod(t, c.duration)
|
||||
: glm::clamp(t, 0.0f, c.duration - 1e-3f);
|
||||
};
|
||||
const AnimationClip& cur = clips[anim.clipIndex];
|
||||
anim.time = wrapTime(anim.time, cur);
|
||||
|
||||
// ---- crossfade 计时 → 求值快照(状态翻转在这里,
|
||||
// 权重数学在求值器)----
|
||||
CrossfadeState xf;
|
||||
if (anim.fadeFrom >= 0) {
|
||||
// 淡出计时(暂停时冻结,与播放头一致)
|
||||
if (anim.playing) anim.fadeElapsed += dt;
|
||||
const f32 f = glm::min(
|
||||
anim.fadeElapsed / glm::max(anim.fadeDuration, 1e-4f),
|
||||
1.0f);
|
||||
xf.wCur = f * f * (3.0f - 2.0f * f); // smoothstep
|
||||
if (f >= 1.0f) {
|
||||
anim.fadeFrom = -1; // 过渡完成
|
||||
} else {
|
||||
xf.src = &clips[anim.fadeFrom];
|
||||
anim.fadeFromTime = wrapTime(anim.fadeFromTime, *xf.src);
|
||||
xf.srcTime = anim.fadeFromTime;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 姿态求值(双层混合在求值器内部)----
|
||||
m_Evaluator.Evaluate(*model, cur, anim.time, xf, m_Globals);
|
||||
PushPalette(entity, model);
|
||||
});
|
||||
|
||||
// ---- 段 2:Controller 模式(状态机推进 + BlendTree 求值,
|
||||
// Unity Animator Update 对应物)----
|
||||
// clip 名查找表按模型每帧重建(attach/detach 会改 clip 表,
|
||||
// 每帧 O(clips) 重建最稳;大世界优化挂点 = 模型版本标记缓存)
|
||||
std::unordered_map<const Model*,
|
||||
std::unordered_map<std::string, u32>> clipLookup;
|
||||
scene.Each<ECS::MeshRendererComponent, ECS::AnimationComponent>(
|
||||
[&](ECS::Entity entity, ECS::MeshRendererComponent& mr,
|
||||
ECS::AnimationComponent& anim) {
|
||||
if (anim.mode !=
|
||||
ECS::AnimationComponent::AnimMode::Controller) {
|
||||
return;
|
||||
}
|
||||
if (!anim.active) return;
|
||||
if (!mr.model.Valid()) return;
|
||||
const Model* model = assets.GetModel(mr.model);
|
||||
if (!model || !model->HasAnimations()) return;
|
||||
const auto* ctrl = assets.FindController(anim.controllerPath);
|
||||
if (!ctrl) return; // 未加载/路径无效 → rest 兜底
|
||||
const auto& states = ctrl->GetStates();
|
||||
if (states.empty()) return;
|
||||
|
||||
// entry 初始化(首帧 / controller 换绑致状态失效)
|
||||
if (anim.currentState.empty() ||
|
||||
!states.count(anim.currentState)) {
|
||||
anim.currentState = ctrl->GetEntryState();
|
||||
anim.stateTime = 0.0f;
|
||||
anim.transFrom.clear();
|
||||
anim.transElapsed = 0.0f;
|
||||
}
|
||||
|
||||
// 参数缺键补声明默认值(emplace 只补缺,调好的值不覆盖):
|
||||
// 覆盖 controller 编辑期新增参数/换绑/手改场景——否则
|
||||
// 新参数在实例 map 缺键,条件读 0 而非声明默认
|
||||
for (const auto& p : ctrl->GetParams()) {
|
||||
if (p.type == ControllerParam::Type::Bool) {
|
||||
anim.boolParams.emplace(p.name, p.defBool);
|
||||
} else if (p.type == ControllerParam::Type::Float) {
|
||||
anim.floatParams.emplace(p.name, p.defFloat);
|
||||
}
|
||||
}
|
||||
|
||||
// clip 名 → 下标表
|
||||
auto& lookup = clipLookup[model];
|
||||
if (lookup.empty()) {
|
||||
const auto& clips = model->GetClips();
|
||||
for (u32 i = 0; i < clips.size(); ++i) {
|
||||
lookup.emplace(clips[i].name, i);
|
||||
}
|
||||
}
|
||||
const auto findClip =
|
||||
[&](const std::string& name) -> const AnimationClip* {
|
||||
const auto it = lookup.find(name);
|
||||
return it != lookup.end()
|
||||
? &model->GetClips()[it->second]
|
||||
: nullptr;
|
||||
};
|
||||
|
||||
const ControllerState* st =
|
||||
&states.at(anim.currentState);
|
||||
// 状态代表 clip(exitTime 判定用:单 clip 直取;
|
||||
// BlendTree 取首子节点——同树内时长通常一致)
|
||||
const auto stateClipOf =
|
||||
[&](const ControllerState& s) -> const AnimationClip* {
|
||||
if (s.tree.type == BlendTree::Type::Single) {
|
||||
return findClip(s.tree.clip);
|
||||
}
|
||||
return s.tree.children.empty()
|
||||
? nullptr
|
||||
: findClip(s.tree.children.front().clip);
|
||||
};
|
||||
const AnimationClip* stateClip = stateClipOf(*st);
|
||||
|
||||
// ---- 时钟推进(组件 speed × 状态 speed)----
|
||||
if (anim.playing && anim.speed != 0.0f) {
|
||||
anim.stateTime += dt * anim.speed * st->speed;
|
||||
if (!anim.transFrom.empty()) {
|
||||
anim.transElapsed += dt;
|
||||
// 来源状态播放头照常推进(过渡期间两状态并行采样);
|
||||
// 来源非 loop 时钳尾(与 Direct fadeFromTime 一致,
|
||||
// 否则采样内部 mod 回绕、淡出期间来源动作循环)
|
||||
if (auto it = states.find(anim.transFrom);
|
||||
it != states.end()) {
|
||||
anim.transFromTime +=
|
||||
dt * anim.speed * it->second.speed;
|
||||
if (const AnimationClip* sc =
|
||||
stateClipOf(it->second);
|
||||
sc && !it->second.loop &&
|
||||
sc->duration > 0.0f) {
|
||||
anim.transFromTime = glm::min(
|
||||
anim.transFromTime,
|
||||
sc->duration - 1e-3f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 非 loop 状态播放头钳尾("播完一轮"判定基准)
|
||||
const bool finished =
|
||||
stateClip && !st->loop && stateClip->duration > 0.0f &&
|
||||
anim.stateTime >= stateClip->duration - 1e-3f;
|
||||
if (stateClip && !st->loop && stateClip->duration > 0.0f) {
|
||||
anim.stateTime = glm::min(
|
||||
anim.stateTime, stateClip->duration - 1e-3f);
|
||||
}
|
||||
|
||||
// ---- 条件评估(数值比较;bool 条件里 trigger 名命中
|
||||
// 视为 true 且一次性消费)----
|
||||
auto evalCond = [&](const TransitionCond& c) -> bool {
|
||||
if (!c.valid) return false;
|
||||
if (c.always) return true;
|
||||
if (c.isBool) {
|
||||
bool v = false;
|
||||
if (auto it = anim.boolParams.find(c.param);
|
||||
it != anim.boolParams.end()) {
|
||||
v = it->second;
|
||||
}
|
||||
for (size_t i = 0; i < anim.pendingTriggers.size();
|
||||
++i) {
|
||||
if (anim.pendingTriggers[i] == c.param) {
|
||||
v = true; // trigger 命中 = 已触发信号
|
||||
anim.pendingTriggers.erase(
|
||||
anim.pendingTriggers.begin() +
|
||||
static_cast<ptrdiff_t>(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return c.op == TransitionCond::Op::Eq
|
||||
? (v == c.boolOperand)
|
||||
: (v != c.boolOperand);
|
||||
}
|
||||
f32 v = 0.0f;
|
||||
if (auto it = anim.floatParams.find(c.param);
|
||||
it != anim.floatParams.end()) {
|
||||
v = it->second;
|
||||
}
|
||||
switch (c.op) {
|
||||
case TransitionCond::Op::Gt: return v > c.numOperand;
|
||||
case TransitionCond::Op::Lt: return v < c.numOperand;
|
||||
case TransitionCond::Op::Ge: return v >= c.numOperand;
|
||||
case TransitionCond::Op::Le: return v <= c.numOperand;
|
||||
case TransitionCond::Op::Eq: return v == c.numOperand;
|
||||
case TransitionCond::Op::Ne: return v != c.numOperand;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// 开过渡(CrossFade 语义:来源冻结为淡出层,目标归零重播)。
|
||||
// to 必须存在:资产可能引用已删除/改名状态(编辑器删态、
|
||||
// 手改 yaml),states.at 越界是崩溃,跳过即可
|
||||
auto fire = [&](const std::string& to, f32 fade) {
|
||||
if (to == anim.currentState || !states.count(to)) return;
|
||||
anim.transFrom = anim.currentState;
|
||||
anim.transFromTime = anim.stateTime;
|
||||
anim.transElapsed = 0.0f;
|
||||
anim.transFade = glm::max(fade, 1e-3f);
|
||||
anim.currentState = to;
|
||||
anim.stateTime = 0.0f;
|
||||
st = &states.at(to);
|
||||
stateClip = stateClipOf(*st);
|
||||
};
|
||||
|
||||
// ---- 出边评估(过渡期间不叠新过渡)。顺序:AnyState
|
||||
// → 当前态出边 → transitionsOnEnd,一帧至多一条 ----
|
||||
if (anim.transFrom.empty()) {
|
||||
for (const auto& tr : ctrl->GetTransitions()) {
|
||||
if (tr.from != "*" || tr.to == anim.currentState)
|
||||
continue;
|
||||
if (tr.hasExitTime && !finished) continue;
|
||||
if (evalCond(tr.cond)) {
|
||||
fire(tr.to, tr.fade);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (anim.transFrom.empty()) {
|
||||
for (const auto& tr : ctrl->GetTransitions()) {
|
||||
if (tr.from != anim.currentState ||
|
||||
tr.to == anim.currentState) {
|
||||
continue;
|
||||
}
|
||||
if (tr.hasExitTime && !finished) continue;
|
||||
if (evalCond(tr.cond)) {
|
||||
fire(tr.to, tr.fade);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (anim.transFrom.empty() && finished &&
|
||||
!st->transitionsOnEnd.empty()) {
|
||||
for (const auto& to : st->transitionsOnEnd) {
|
||||
if (states.count(to) && to != anim.currentState) {
|
||||
fire(to, 0.25f); // 播完出口缺省 fade
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- BlendTree → PoseLayer[](2D 方向树按角度扇区
|
||||
// 裁剪,通常仅 2-3 层权重>0)----
|
||||
auto buildLayers =
|
||||
[&](std::vector<PoseLayer>& out, const ControllerState& s,
|
||||
f32 time) {
|
||||
out.clear();
|
||||
const BlendTree& tree = s.tree;
|
||||
if (tree.type == BlendTree::Type::Single) {
|
||||
if (const auto* c = findClip(tree.clip)) {
|
||||
out.push_back({c, time, 1.0f});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const auto param = [&](const std::string& n) -> f32 {
|
||||
auto it = anim.floatParams.find(n);
|
||||
return it != anim.floatParams.end() ? it->second
|
||||
: 0.0f;
|
||||
};
|
||||
const auto& ch = tree.children;
|
||||
if (ch.empty()) return;
|
||||
|
||||
if (tree.type == BlendTree::Type::Directional1D) {
|
||||
// 一轴阈值:端点外钳单层,区间内相邻对插值
|
||||
const f32 x = param(tree.xParam);
|
||||
if (x <= ch.front().pos.x) {
|
||||
if (const auto* c =
|
||||
findClip(ch.front().clip)) {
|
||||
out.push_back({c, time, 1.0f});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (x >= ch.back().pos.x) {
|
||||
if (const auto* c = findClip(ch.back().clip)) {
|
||||
out.push_back({c, time, 1.0f});
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i + 1 < ch.size(); ++i) {
|
||||
if (x >= ch[i].pos.x &&
|
||||
x <= ch[i + 1].pos.x) {
|
||||
const f32 span =
|
||||
ch[i + 1].pos.x - ch[i].pos.x;
|
||||
const f32 t =
|
||||
span > 1e-6f
|
||||
? (x - ch[i].pos.x) / span
|
||||
: 0.0f;
|
||||
if (const auto* c =
|
||||
findClip(ch[i].clip)) {
|
||||
out.push_back({c, time, 1.0f - t});
|
||||
}
|
||||
if (const auto* c =
|
||||
findClip(ch[i + 1].clip)) {
|
||||
out.push_back({c, time, t});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 2D 方向混合:权重数学单一事实源 =
|
||||
// EvaluateBlendTree2D(编辑器方向盘可视化共用,
|
||||
// 两边永不漂移)
|
||||
EvaluateBlendTree2D(tree, param(tree.xParam),
|
||||
param(tree.zParam),
|
||||
m_BlendWeights);
|
||||
for (const auto& [ci, w] : m_BlendWeights) {
|
||||
if (const auto* c = findClip(ch[ci].clip)) {
|
||||
out.push_back({c, time, w});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
buildLayers(m_LayerScratch, *st, anim.stateTime);
|
||||
if (m_LayerScratch.empty()) {
|
||||
// 层全空兜底(防 T-pose 跳变):children 里首个
|
||||
// 有效 clip 单层顶上——典型场景 = 2D 树没有原点
|
||||
// 子节点时 v 回到中心(方向层权重全被最小阈值滤
|
||||
// 掉),或 clip 名全失效。rest 兜底(T-pose)只
|
||||
// 留给 children 全无效的配置错误
|
||||
const BlendTree& tree = st->tree;
|
||||
const auto firstValid = [&](const auto& cs)
|
||||
-> const AnimationClip* {
|
||||
for (const auto& c : cs) {
|
||||
if (const auto* clip = findClip(c.clip)) {
|
||||
return clip;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
const AnimationClip* fb =
|
||||
tree.type == BlendTree::Type::Single
|
||||
? findClip(tree.clip)
|
||||
: firstValid(tree.children);
|
||||
if (fb != nullptr) {
|
||||
m_LayerScratch.push_back({fb, anim.stateTime, 1.0f});
|
||||
}
|
||||
}
|
||||
if (!anim.transFrom.empty()) {
|
||||
const f32 f = glm::min(anim.transElapsed / anim.transFade,
|
||||
1.0f);
|
||||
if (f >= 1.0f) {
|
||||
anim.transFrom.clear(); // 过渡完成
|
||||
} else {
|
||||
// 当前层权重 ×t,来源层权重 ×(1-t),合并求值
|
||||
const f32 t = f * f * (3.0f - 2.0f * f);
|
||||
buildLayers(m_FromLayers,
|
||||
states.at(anim.transFrom),
|
||||
anim.transFromTime);
|
||||
for (auto& l : m_LayerScratch) l.weight *= t;
|
||||
for (auto& l : m_FromLayers) {
|
||||
l.weight *= (1.0f - t);
|
||||
m_LayerScratch.push_back(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_Evaluator.EvaluateLayers(*model, m_LayerScratch.data(),
|
||||
static_cast<u32>(
|
||||
m_LayerScratch.size()),
|
||||
m_Globals);
|
||||
PushPalette(entity, model);
|
||||
});
|
||||
|
||||
// ---- 蒙皮但无动画采样路径的实体:rest-pose palette 兜底 ----
|
||||
// identity 槽只对"顶点烘焙进模型空间"的资产等于绑定姿势;
|
||||
// assimp 导入的蒙皮顶点在骨骼局部空间,identity 直通渲染成
|
||||
// 乱码(Xbot.glb / M1911.fbx 无动画组件实证)。绑定姿势的正确
|
||||
// 语义 = rest 姿态 palette(求值器 rest 求值同源)。rest 是模型
|
||||
// 静态属性:同模型全部实体共享同一槽位段,每模型每帧只算一次
|
||||
std::unordered_map<const Model*, u32> restBase;
|
||||
scene.Each<ECS::MeshRendererComponent>(
|
||||
[&](ECS::Entity entity, ECS::MeshRendererComponent& mr) {
|
||||
if (!mr.model.Valid()) return;
|
||||
const u32 entKey =
|
||||
static_cast<u32>(entt::to_integral(entity));
|
||||
if (frame.entitySkinBases.count(entKey)) return;
|
||||
const Model* model = assets.GetModel(mr.model);
|
||||
if (!model || model->GetBones().empty()) return;
|
||||
if (auto it = restBase.find(model); it != restBase.end()) {
|
||||
frame.entitySkinBases[entKey] = it->second;
|
||||
return;
|
||||
}
|
||||
const auto& bones = model->GetBones();
|
||||
const u32 base =
|
||||
static_cast<u32>(frame.skinPalettes.size());
|
||||
if (base + bones.size() > kMaxSkinMatrices) {
|
||||
frame.skinPaletteTruncated = true;
|
||||
return; // 预算打满 = 该模型实体退回 identity 槽
|
||||
}
|
||||
m_Evaluator.ComputeRestPose(*model, m_Globals);
|
||||
for (const auto& b : bones) {
|
||||
const u32 ni = b.nodeIdx < m_Globals.size()
|
||||
? b.nodeIdx : 0;
|
||||
frame.skinPalettes.push_back(
|
||||
m_Globals[ni] * b.invBind);
|
||||
}
|
||||
restBase.emplace(model, base);
|
||||
frame.entitySkinBases[entKey] = base;
|
||||
});
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
EmitSelectedSkeleton(scene, assets, frame);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
|
||||
void AnimationSystem::EmitSelectedSkeleton(ECS::Scene& scene,
|
||||
Assets::AssetManager& assets,
|
||||
FrameSceneData& frame)
|
||||
{
|
||||
// 总开关(3D 骨架线 + 关节圆点一起关;boneJoints 已在 Update 清)
|
||||
if (!frame.showBoneWires) return;
|
||||
// PIE 运行态整体停产:PIE 画面 = primary 相机渲染的游戏,编辑器
|
||||
// 可视化(骨架线/关节圆点)叠上去必然错位(同 gizmo 收集门控)
|
||||
if (frame.pieActive) return;
|
||||
if (!frame.selectedEntityValid) return;
|
||||
const auto entity = static_cast<ECS::Entity>(frame.selectedEntityBits);
|
||||
auto& reg = scene.GetRegistry();
|
||||
if (!reg.valid(entity)) return;
|
||||
const auto* t = reg.try_get<ECS::TransformComponent>(entity);
|
||||
const auto* mr = reg.try_get<ECS::MeshRendererComponent>(entity);
|
||||
if (!t || !mr || !mr->model.Valid()) return;
|
||||
const Model* model = assets.GetModel(mr->model);
|
||||
if (!model || model->GetNodes().empty()) return;
|
||||
|
||||
// 姿态来源:当帧求值命中 → 用暂存姿态;否则绑定姿势
|
||||
//(与渲染 identity palette 表现一致——组件禁用/无动画的实体
|
||||
// 在渲染里就是绑定姿势,可视化不得显示别的姿态)
|
||||
if (!m_SelHasPose) {
|
||||
m_Evaluator.ComputeRestPose(*model, m_SelGlobals);
|
||||
}
|
||||
|
||||
// 骨骼成员标记(boneAllNodes=false 时只画骨骼链;bone 表里的
|
||||
// node 才是"骨骼",纯 IK/辅助节点画出来只会糊)
|
||||
const auto& nodes = model->GetNodes();
|
||||
const auto& bones = model->GetBones();
|
||||
std::vector<u8> isBone(nodes.size(), 0);
|
||||
for (const auto& b : bones) {
|
||||
if (b.nodeIdx < isBone.size()) isBone[b.nodeIdx] = 1;
|
||||
}
|
||||
|
||||
// ---- 3D 骨架线(EditorGizmoPass LineList 直绘)----
|
||||
// 骨骼线亮橙 / 普通节点线暗灰,层级一眼分层。"仅骨骼"模式 =
|
||||
// 子节点是骨骼才画(其父链自然衔接,链不会断)
|
||||
const glm::mat4& world = t->World();
|
||||
u32 pairCount = 0;
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
if (nodes[i].parent >= 0 &&
|
||||
(frame.boneAllNodes || isBone[i])) {
|
||||
++pairCount;
|
||||
}
|
||||
}
|
||||
frame.boneWireVerts.reserve(frame.boneWireVerts.size() + pairCount * 2);
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
if (nodes[i].parent < 0) continue;
|
||||
if (!frame.boneAllNodes && !isBone[i]) continue;
|
||||
const u32 p = static_cast<u32>(nodes[i].parent);
|
||||
const glm::vec3 a =
|
||||
glm::vec3(world * m_SelGlobals[p] * glm::vec4(0, 0, 0, 1));
|
||||
const glm::vec3 b =
|
||||
glm::vec3(world * m_SelGlobals[i] * glm::vec4(0, 0, 0, 1));
|
||||
const glm::vec4& color = (frame.boneAllNodes && !isBone[i])
|
||||
? kGizmoNodeDimColor
|
||||
: kGizmoSelectedColor;
|
||||
frame.boneWireVerts.push_back({a, color});
|
||||
frame.boneWireVerts.push_back({b, color});
|
||||
}
|
||||
|
||||
// ---- 关节标记(ViewportPanel overlay 圆点 + 骨骼名 tooltip,
|
||||
// Endfield-Poser 范式:圆点是可读性主体)----
|
||||
frame.boneJoints.reserve(frame.boneJoints.size() + nodes.size());
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
if (!frame.boneAllNodes && !isBone[i]) continue;
|
||||
frame.boneJoints.push_back(
|
||||
{glm::vec3(world * m_SelGlobals[i] * glm::vec4(0, 0, 0, 1)),
|
||||
i, isBone[i]});
|
||||
}
|
||||
}
|
||||
|
||||
#endif // HELLO_EDITOR
|
||||
|
||||
} // namespace Engine::Animation
|
||||
@@ -0,0 +1,246 @@
|
||||
//
|
||||
// HumanoidRig 实现:关键词表 + 归一化 + 自动映射。
|
||||
//
|
||||
|
||||
#include <Engine/Animation/HumanoidRig.h>
|
||||
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Engine::Animation {
|
||||
|
||||
namespace {
|
||||
|
||||
// ---- 名字归一化:拆 (body, side) ----
|
||||
//
|
||||
// 规则:
|
||||
// 1) 去 "mixamorig:" 等前缀
|
||||
// 2) 小写;分隔符(空格/_/.-:)一律删除
|
||||
// 3) left/right(Mixamo "LeftArm")与 左/右(MMD "左腕")统一
|
||||
// 抽成 side,body 里剔除;.L/.R、_L/_R 后缀同上
|
||||
//
|
||||
// body 用**精确相等**匹配关键词(非子串)——"lowerarm"
|
||||
// 不会误命中 "arm"
|
||||
|
||||
Side ExtractSide(std::string& body)
|
||||
{
|
||||
Side s = Side::None;
|
||||
auto cut = [&](std::string_view token, Side which)
|
||||
{
|
||||
size_t pos = 0;
|
||||
while ((pos = body.find(token, pos)) != std::string::npos) {
|
||||
body.erase(pos, token.size());
|
||||
if (s == Side::None) s = which;
|
||||
}
|
||||
};
|
||||
cut("left", Side::Left);
|
||||
cut("right", Side::Right);
|
||||
cut("左", Side::Left); // MMD 前缀
|
||||
cut("右", Side::Right);
|
||||
cut(".l", Side::Left); // blender 惯例后缀
|
||||
cut(".r", Side::Right);
|
||||
cut("_l", Side::Left);
|
||||
cut("_r", Side::Right);
|
||||
return s;
|
||||
}
|
||||
|
||||
// ① 前缀剥离 + 小写(分隔符**保留**——后缀式 side 标记
|
||||
// ".l"/"_l" 依赖分隔符存在,必须先于分隔符删除识别)
|
||||
std::string NormalizeLower(std::string_view name)
|
||||
{
|
||||
std::string s(name);
|
||||
constexpr std::string_view kMixamo = "mixamorig";
|
||||
if (s.rfind(kMixamo, 0) == 0) {
|
||||
s.erase(0, kMixamo.size());
|
||||
if (!s.empty() && (s[0] == ':' || s[0] == '_' ||
|
||||
s[0] == '-' || s[0] == '.')) {
|
||||
s.erase(0, 1);
|
||||
}
|
||||
}
|
||||
for (char& c : s) {
|
||||
c = static_cast<char>(std::tolower(
|
||||
static_cast<unsigned char>(c)));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// ② 分隔符删除(在 ExtractSide 之后调用;CJK 多字节原样过)
|
||||
std::string StripSeparators(const std::string& s)
|
||||
{
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (char c : s) {
|
||||
if (std::isalnum(static_cast<unsigned char>(c)) ||
|
||||
static_cast<unsigned char>(c) >= 0x80) {
|
||||
out.push_back(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- body → 左侧槽位 关键词表(右侧 = 左槽 + 段偏移)----
|
||||
// 精确相等匹配;顺序即优先级(先命中先用)
|
||||
struct BodyRule {
|
||||
std::string_view body;
|
||||
HumanoidSlot left;
|
||||
};
|
||||
|
||||
constexpr std::array<BodyRule, 43> kBodyRules{{
|
||||
// 主干(MMD 语义:下半身=hips、上半身链=spine→chest)
|
||||
{"hips", HumanoidSlot::Hips},
|
||||
{"pelvis", HumanoidSlot::Hips},
|
||||
{"bip01", HumanoidSlot::Hips},
|
||||
{"下半身", HumanoidSlot::Hips},
|
||||
{"センター", HumanoidSlot::Hips},
|
||||
{"spine", HumanoidSlot::Spine},
|
||||
{"上半身", HumanoidSlot::Spine},
|
||||
{"chest", HumanoidSlot::Chest},
|
||||
{"spine1", HumanoidSlot::Chest},
|
||||
{"上半身1", HumanoidSlot::Chest},
|
||||
{"upperchest", HumanoidSlot::UpperChest},
|
||||
{"spine2", HumanoidSlot::UpperChest},
|
||||
{"上半身2", HumanoidSlot::UpperChest},
|
||||
{"neck", HumanoidSlot::Neck},
|
||||
{"首", HumanoidSlot::Neck},
|
||||
{"head", HumanoidSlot::Head},
|
||||
{"頭", HumanoidSlot::Head},
|
||||
// 臂段(MMD:腕=upper arm、ひじ/肘=elbow、手首=wrist)
|
||||
{"shoulder", HumanoidSlot::LeftShoulder},
|
||||
{"clavicle", HumanoidSlot::LeftShoulder},
|
||||
{"肩", HumanoidSlot::LeftShoulder},
|
||||
{"arm", HumanoidSlot::LeftUpperArm},
|
||||
{"upperarm", HumanoidSlot::LeftUpperArm},
|
||||
{"bicep", HumanoidSlot::LeftUpperArm},
|
||||
{"腕", HumanoidSlot::LeftUpperArm},
|
||||
{"lowerarm", HumanoidSlot::LeftLowerArm},
|
||||
{"forearm", HumanoidSlot::LeftLowerArm},
|
||||
{"elbow", HumanoidSlot::LeftLowerArm},
|
||||
{"ひじ", HumanoidSlot::LeftLowerArm},
|
||||
{"肘", HumanoidSlot::LeftLowerArm},
|
||||
{"hand", HumanoidSlot::LeftHand},
|
||||
{"手首", HumanoidSlot::LeftHand},
|
||||
// 腿段(MMD:足=thigh、ひざ=knee、足首=ankle)
|
||||
{"upperleg", HumanoidSlot::LeftUpperLeg},
|
||||
{"upleg", HumanoidSlot::LeftUpperLeg},
|
||||
{"thigh", HumanoidSlot::LeftUpperLeg},
|
||||
{"足", HumanoidSlot::LeftUpperLeg},
|
||||
{"lowerleg", HumanoidSlot::LeftLowerLeg},
|
||||
{"leg", HumanoidSlot::LeftLowerLeg}, // Mixamo:LeftLeg=小腿(大腿是 UpLeg)
|
||||
{"knee", HumanoidSlot::LeftLowerLeg},
|
||||
{"calf", HumanoidSlot::LeftLowerLeg},
|
||||
{"ひざ", HumanoidSlot::LeftLowerLeg},
|
||||
{"foot", HumanoidSlot::LeftFoot},
|
||||
{"ankle", HumanoidSlot::LeftFoot},
|
||||
{"足首", HumanoidSlot::LeftFoot},
|
||||
}};
|
||||
|
||||
// 左→右槽位偏移:臂/肩段(Shoulder..Hand)+4,腿段(UpperLeg..Foot)+3
|
||||
HumanoidSlot ToSide(HumanoidSlot leftSlot, Side side)
|
||||
{
|
||||
if (side != Side::Right) return leftSlot;
|
||||
const u32 s = static_cast<u32>(leftSlot);
|
||||
const u32 legStart = static_cast<u32>(HumanoidSlot::LeftUpperLeg);
|
||||
return static_cast<HumanoidSlot>(s + (s >= legStart ? 3 : 4));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* HumanoidSlotName(HumanoidSlot slot)
|
||||
{
|
||||
static constexpr std::array<const char*, 20> kNames{
|
||||
"Hips", "Spine", "Chest", "UpperChest", "Neck", "Head",
|
||||
"LeftShoulder", "LeftUpperArm", "LeftLowerArm", "LeftHand",
|
||||
"RightShoulder", "RightUpperArm", "RightLowerArm", "RightHand",
|
||||
"LeftUpperLeg", "LeftLowerLeg", "LeftFoot",
|
||||
"RightUpperLeg", "RightLowerLeg", "RightFoot",
|
||||
};
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
return s < kNames.size() ? kNames[s] : "?";
|
||||
}
|
||||
|
||||
HumanoidSlot MatchHumanoidSlot(std::string_view name, Side& side,
|
||||
bool& ok)
|
||||
{
|
||||
side = Side::None;
|
||||
ok = false;
|
||||
std::string body = NormalizeLower(name);
|
||||
side = ExtractSide(body);
|
||||
body = StripSeparators(body);
|
||||
if (body.empty()) return HumanoidSlot::Hips;
|
||||
|
||||
for (const BodyRule& r : kBodyRules) {
|
||||
if (body == r.body) {
|
||||
ok = true;
|
||||
return ToSide(r.left, side);
|
||||
}
|
||||
}
|
||||
return HumanoidSlot::Hips;
|
||||
}
|
||||
|
||||
std::vector<std::pair<u32, u32>> AutoMapHumanoid(
|
||||
const std::vector<ModelNode>& srcNodes,
|
||||
const std::vector<ModelNode>& dstNodes)
|
||||
{
|
||||
std::vector<std::pair<u32, u32>> pairs;
|
||||
|
||||
// 1) 同名直映(同源拆分资产零损耗路径)
|
||||
std::unordered_map<std::string, u32> dstByName;
|
||||
dstByName.reserve(dstNodes.size());
|
||||
for (u32 i = 0; i < dstNodes.size(); ++i) {
|
||||
dstByName.emplace(dstNodes[i].name, i);
|
||||
}
|
||||
std::vector<u8> dstUsed(dstNodes.size(), 0);
|
||||
std::vector<u8> srcMapped(srcNodes.size(), 0);
|
||||
for (u32 i = 0; i < srcNodes.size(); ++i) {
|
||||
if (auto it = dstByName.find(srcNodes[i].name);
|
||||
it != dstByName.end() && !dstUsed[it->second]) {
|
||||
pairs.emplace_back(i, it->second);
|
||||
dstUsed[it->second] = 1;
|
||||
srcMapped[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 槽位匹配:两侧各自识别 (slot → nodeIdx),槽位一对一配对
|
||||
constexpr u32 kSlotCount = static_cast<u32>(HumanoidSlot::Count);
|
||||
auto slotOf = [](const std::vector<ModelNode>& nodes,
|
||||
std::array<i32, kSlotCount>& out)
|
||||
{
|
||||
out.fill(-1);
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
Side side;
|
||||
bool ok = false;
|
||||
const HumanoidSlot slot =
|
||||
MatchHumanoidSlot(nodes[i].name, side, ok);
|
||||
if (!ok) {
|
||||
continue;
|
||||
}
|
||||
const u32 s = static_cast<u32>(slot);
|
||||
if (out[s] < 0) {
|
||||
out[s] = static_cast<i32>(i); // 关键词先命中优先
|
||||
}
|
||||
}
|
||||
};
|
||||
std::array<i32, kSlotCount> srcSlot{};
|
||||
std::array<i32, kSlotCount> dstSlot{};
|
||||
slotOf(srcNodes, srcSlot);
|
||||
slotOf(dstNodes, dstSlot);
|
||||
|
||||
for (u32 s = 0; s < kSlotCount; ++s) {
|
||||
if (srcSlot[s] < 0 || dstSlot[s] < 0) {
|
||||
continue;
|
||||
}
|
||||
const u32 si = static_cast<u32>(srcSlot[s]);
|
||||
const u32 di = static_cast<u32>(dstSlot[s]);
|
||||
if (srcMapped[si] || dstUsed[di]) {
|
||||
continue; // 同名直映已覆盖
|
||||
}
|
||||
pairs.emplace_back(si, di);
|
||||
dstUsed[di] = 1;
|
||||
srcMapped[si] = 1;
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
} // namespace Engine::Animation
|
||||
@@ -132,92 +132,52 @@ namespace Engine::Assets {
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 材质资产(.material)----
|
||||
|
||||
MaterialAssetRef AssetManager::LoadMaterialAsset(const std::string& path)
|
||||
{
|
||||
// YAML 解析 + GPU 构建同步串行(主线程低频路径:场景预载 /
|
||||
// 编辑器指派)。解析失败或 GPU 构建失败 → 无效句柄
|
||||
return m_Materials.Load(path, [this](const std::string& p) {
|
||||
MaterialAssetData data;
|
||||
if (!MaterialAsset::Load(p, data)) {
|
||||
return Scope<MaterialAsset>{};
|
||||
}
|
||||
return m_Uploader.CreateMaterial(std::move(data));
|
||||
});
|
||||
}
|
||||
|
||||
AssetManager::AnimCtrlRef AssetManager::LoadController(const std::string& path)
|
||||
{
|
||||
// 纯 CPU 解析(无 GPU 资源),解析失败返回无效句柄
|
||||
return m_Controllers.Load(path, [](const std::string& p) {
|
||||
return Animation::AnimationController::Load(p);
|
||||
});
|
||||
}
|
||||
|
||||
MaterialAssetRef AssetManager::ReloadMaterialAsset(const std::string& path)
|
||||
{
|
||||
// GPU 全停飞保证旧纹理/描述符无在飞引用(waitIdle 经 uploader
|
||||
// 接缝,与 SetEnvironment 换绑同一先例);unload → reload,
|
||||
// generation 递增让旧句柄自然失效,引用方下帧 resolve 拿新实例
|
||||
m_Uploader.WaitDeviceIdle();
|
||||
m_Materials.UnloadByPath(path);
|
||||
return LoadMaterialAsset(path);
|
||||
}
|
||||
|
||||
AssetManager::AnimAttachResult AssetManager::AttachAnimationClips(
|
||||
ModelRef target, const std::string& animPath)
|
||||
{
|
||||
AnimAttachResult res;
|
||||
// facade:取模型 + 独立解析动画容器(不复用缓存里可能已预热的
|
||||
// 同名 Model——缓存实例的 clips nodeIdx 按它自己的 node 表编号,
|
||||
// attach 需要按目标模型的 node 表重映射),重映射算法在
|
||||
// Animation::AttachClipsToModel(幂等/去重/告警语义全在彼处)
|
||||
Model* tm = GetModel(target);
|
||||
if (tm == nullptr) {
|
||||
return res;
|
||||
return {};
|
||||
}
|
||||
// 幂等:该源文件已并入过(自愈重放/重复 attach 直接返回)
|
||||
for (const auto& c : tm->GetClips()) {
|
||||
if (c.sourcePath == animPath) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
// 纯 CPU 解析:动画容器只要 nodes + clips。不复用缓存里可能已
|
||||
// 预热的同名 Model——缓存实例的 clips nodeIdx 按它自己的 node 表
|
||||
// 编号,attach 需要按目标模型的 node 表重映射,必须独立解析
|
||||
ParsedModel src = Model::ParseFromDisk(animPath);
|
||||
if (src.clips.empty()) {
|
||||
ENGINE_WARN("AttachAnimationClips: no clips in '{}'", animPath);
|
||||
return res;
|
||||
}
|
||||
|
||||
// 目标模型 骨骼节点名 → nodeIdx 重映射表
|
||||
std::unordered_map<std::string, u32> nameToIdx;
|
||||
nameToIdx.reserve(tm->GetNodes().size());
|
||||
for (u32 i = 0; i < tm->GetNodes().size(); ++i) {
|
||||
nameToIdx.emplace(tm->GetNodes()[i].name, i);
|
||||
}
|
||||
|
||||
const std::string stem =
|
||||
std::filesystem::path(animPath).stem().string();
|
||||
std::vector<AnimationClip> extra;
|
||||
extra.reserve(src.clips.size());
|
||||
for (auto& clip : src.clips) {
|
||||
AnimationClip out;
|
||||
out.name = clip.name;
|
||||
out.duration = clip.duration;
|
||||
out.tps = clip.tps;
|
||||
out.sourcePath = animPath;
|
||||
// 同名冲突(内嵌 clip 或其它源):源文件名做前缀保持可辨识
|
||||
for (const auto& c : tm->GetClips()) {
|
||||
if (c.name == out.name) {
|
||||
out.name = stem + "_" + out.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (auto& ch : clip.channels) {
|
||||
if (ch.positions.empty() && ch.rotations.empty() &&
|
||||
ch.scales.empty()) {
|
||||
continue; // 空轨道无用
|
||||
}
|
||||
if (ch.nodeIdx >= src.nodes.size()) {
|
||||
++res.channelsSkipped;
|
||||
continue;
|
||||
}
|
||||
const auto it = nameToIdx.find(src.nodes[ch.nodeIdx].name);
|
||||
if (it == nameToIdx.end()) {
|
||||
++res.channelsSkipped; // 骨骼名不同源
|
||||
continue;
|
||||
}
|
||||
AnimChannel remapped;
|
||||
remapped.nodeIdx = it->second;
|
||||
remapped.positions = std::move(ch.positions);
|
||||
remapped.rotations = std::move(ch.rotations);
|
||||
remapped.scales = std::move(ch.scales);
|
||||
out.channels.push_back(std::move(remapped));
|
||||
++res.channelsMatched;
|
||||
}
|
||||
// 全部轨道对不上 = 名字空间完全不同源,clip 不入表
|
||||
if (out.channels.empty()) {
|
||||
ENGINE_WARN("AttachAnimationClips: clip '{}' has no matching "
|
||||
"bones in target (skipped)", clip.name);
|
||||
continue;
|
||||
}
|
||||
extra.push_back(std::move(out));
|
||||
++res.clipsAdded;
|
||||
}
|
||||
if (!extra.empty()) {
|
||||
tm->AppendClips(std::move(extra));
|
||||
}
|
||||
ENGINE_INFO("AttachAnimationClips: '{}' -> {} clips "
|
||||
"({} channels matched, {} skipped)",
|
||||
animPath, res.clipsAdded, res.channelsMatched,
|
||||
res.channelsSkipped);
|
||||
return res;
|
||||
return Animation::AttachClipsToModel(
|
||||
*tm, Model::ParseFromDisk(animPath), animPath);
|
||||
}
|
||||
|
||||
ModelRef AssetManager::CreatePlane(f32 halfSize, u8 r, u8 g, u8 b)
|
||||
|
||||
@@ -95,9 +95,12 @@ namespace Engine {
|
||||
// 后端销毁自己的管线/字体资源前先等 GPU 空闲(层先于 Renderer 析构)
|
||||
m_Device.GetDevice().waitIdle();
|
||||
|
||||
// 场景纹理描述符先归还(后端 shutdown 前必须完成)
|
||||
// 场景纹理 + 材质球预览 atlas 描述符先归还(后端 shutdown 前必须完成)
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
UpdateSceneTexture(slot, vk::ImageView{});
|
||||
#ifdef HELLO_EDITOR
|
||||
UpdatePreviewTexture(slot, vk::ImageView{});
|
||||
#endif
|
||||
}
|
||||
|
||||
ImGui_ImplVulkan_Shutdown();
|
||||
@@ -129,6 +132,25 @@ namespace Engine {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
void ImGuiLayer::UpdatePreviewTexture(u32 slot, vk::ImageView view)
|
||||
{
|
||||
if (slot >= kMaxFramesInFlight) return;
|
||||
|
||||
// 语义与 UpdateSceneTexture 完全一致(回收旧描述符 + 注册新 view)
|
||||
if (m_PreviewTextures[slot]) {
|
||||
ImGui_ImplVulkan_RemoveTexture(
|
||||
(VkDescriptorSet)(intptr_t)m_PreviewTextures[slot]);
|
||||
m_PreviewTextures[slot] = ImTextureID_Invalid;
|
||||
}
|
||||
if (view) {
|
||||
m_PreviewTextures[slot] = (ImTextureID)(intptr_t)
|
||||
ImGui_ImplVulkan_AddTexture(
|
||||
(VkImageView)view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ImGuiLayer::NewFrame()
|
||||
{
|
||||
ImGui_ImplVulkan_NewFrame();
|
||||
|
||||
@@ -14,19 +14,6 @@
|
||||
|
||||
namespace Engine {
|
||||
|
||||
namespace {
|
||||
// 欧拉(度)→ 旋转矩阵:应用序 Y→X,与 TransformComponent::
|
||||
// Compose 一致(roll 不参与 FPS/编辑器相机)
|
||||
glm::mat3 EulerRotMat(const glm::vec3& rotDeg)
|
||||
{
|
||||
return static_cast<glm::mat3>(
|
||||
glm::rotate(glm::mat4(1.0f), glm::radians(rotDeg.y),
|
||||
glm::vec3(0, 1, 0)) *
|
||||
glm::rotate(glm::mat4(1.0f), glm::radians(rotDeg.x),
|
||||
glm::vec3(1, 0, 0)));
|
||||
}
|
||||
}
|
||||
|
||||
void Camera::Update(ECS::TransformComponent& transform,
|
||||
const f32 deltaTime, const bool flyRequiresRMB)
|
||||
{
|
||||
@@ -51,31 +38,29 @@ namespace Engine {
|
||||
|
||||
if (mouseDX != 0 || mouseDY != 0)
|
||||
{
|
||||
glm::vec3 rot = transform.GetRotation();
|
||||
// GetRotation 是 0-360 显示域(UE Details 约定);pitch 的
|
||||
// clamp 需要连续域,wrap 回 [-180,180)(yaw 同步处理,
|
||||
// 跨 ±180 继续累减不打折返)
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
rot[i] = std::fmod(rot[i], 360.0f);
|
||||
if (rot[i] >= 180.0f) rot[i] -= 360.0f;
|
||||
if (rot[i] < -180.0f) rot[i] += 360.0f;
|
||||
}
|
||||
// 右手系绕 Y 正角度 = 向左转,鼠标右移(DX>0)应看向右
|
||||
// → yaw 取负。pitch 正角度 = 抬头,鼠标下移(DY>0)低头
|
||||
// → rot.x 直接用负号,两者符号已核对一致
|
||||
rot.y -= mouseDX * m_MouseSensitivity;
|
||||
rot.x = glm::clamp(
|
||||
rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f);
|
||||
transform.SetRotation(rot);
|
||||
|
||||
if (wantOrbit)
|
||||
if (wantLook)
|
||||
{
|
||||
if (Input::IsKeyDown(Input::Key::LeftShift))
|
||||
// ---- 6DOF 真自由视角:pitch/yaw 各绕本地轴右乘增量
|
||||
// 四元数(q = q·Δyaw·Δpitch),无欧拉读改写、无
|
||||
// ±89° 钳制——可过顶/翻滚,存储层本就是四元数,
|
||||
// 不存在万向锁。符号沿用旧约定:鼠标右移向右看
|
||||
// (右手系绕 Y 正角向左 → yaw 取负)、鼠标下移
|
||||
// 低头(绕 X 正角抬头 → pitch 取负)----
|
||||
const glm::quat dyaw = glm::angleAxis(
|
||||
glm::radians(-mouseDX * m_MouseSensitivity),
|
||||
glm::vec3(0, 1, 0));
|
||||
const glm::quat dpitch = glm::angleAxis(
|
||||
glm::radians(-mouseDY * m_MouseSensitivity),
|
||||
glm::vec3(1, 0, 0));
|
||||
transform.SetRotationQuat(
|
||||
transform.GetRotationQuat() * dyaw * dpitch);
|
||||
}
|
||||
else if (Input::IsKeyDown(Input::Key::LeftShift))
|
||||
{
|
||||
// ---- 屏幕平面平移:内容跟手(拖右 → 相机左移、
|
||||
// 拖下 → 相机上移),pivot 同步平移保持环绕点的
|
||||
// 屏幕相对位置 ----
|
||||
const glm::mat3 r = EulerRotMat(rot);
|
||||
const glm::mat3 r(transform.GetRotationQuat());
|
||||
const glm::vec3 delta =
|
||||
(-r * glm::vec3(1, 0, 0)) * mouseDX +
|
||||
(r * glm::vec3(0, 1, 0)) * mouseDY;
|
||||
@@ -85,23 +70,34 @@ namespace Engine {
|
||||
}
|
||||
else
|
||||
{
|
||||
// ---- 焦点环绕:转向后保持到 pivot 的距离。首次
|
||||
// 环绕无 pivot:取当前视线上 m_OrbitDistance 处
|
||||
// ---- 焦点环绕:世界系左乘增量(yaw 绕世界 Y、pitch
|
||||
// 绕相机右轴),roll 保持不变;位置绕 pivot 同
|
||||
// 角度刚体旋转 → 到 pivot 距离恒定。首次环绕无
|
||||
// pivot:取当前视线上 m_OrbitDistance 处 ----
|
||||
if (!m_HasPivot)
|
||||
{
|
||||
const glm::mat3 r(transform.GetRotationQuat());
|
||||
m_OrbitPivot = transform.GetPosition() +
|
||||
EulerRotMat(transform.GetRotation()) *
|
||||
glm::vec3(0, 0, -1) *
|
||||
(r * glm::vec3(0, 0, -1)) *
|
||||
m_OrbitDistance;
|
||||
m_HasPivot = true;
|
||||
}
|
||||
m_OrbitDistance = glm::length(
|
||||
transform.GetPosition() - m_OrbitPivot);
|
||||
const glm::vec3 orbitFwd =
|
||||
EulerRotMat(rot) * glm::vec3(0, 0, -1);
|
||||
transform.SetPosition(m_OrbitPivot -
|
||||
orbitFwd * m_OrbitDistance);
|
||||
}
|
||||
const glm::vec3 offset =
|
||||
transform.GetPosition() - m_OrbitPivot;
|
||||
m_OrbitDistance = glm::length(offset);
|
||||
const glm::vec3 camRight =
|
||||
glm::mat3(transform.GetRotationQuat()) *
|
||||
glm::vec3(1, 0, 0);
|
||||
const glm::quat dq =
|
||||
glm::angleAxis(
|
||||
glm::radians(-mouseDX * m_MouseSensitivity),
|
||||
glm::vec3(0, 1, 0)) *
|
||||
glm::angleAxis(
|
||||
glm::radians(-mouseDY * m_MouseSensitivity),
|
||||
camRight);
|
||||
// 朝向与位置共用同一 dq:视线始终精确穿过 pivot
|
||||
transform.SetRotationQuat(dq * transform.GetRotationQuat());
|
||||
transform.SetPosition(m_OrbitPivot + dq * offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,7 +107,7 @@ namespace Engine {
|
||||
}
|
||||
|
||||
// 基向量由(可能已更新的)实体旋转决定(本地 -Z 为视线方向)
|
||||
const glm::mat3 r = EulerRotMat(transform.GetRotation());
|
||||
const glm::mat3 r(transform.GetRotationQuat());
|
||||
const glm::vec3 forward = r * glm::vec3(0, 0, -1);
|
||||
const glm::vec3 right = r * glm::vec3(1, 0, 0);
|
||||
|
||||
@@ -150,7 +146,7 @@ namespace Engine {
|
||||
void Camera::FocusOn(ECS::TransformComponent& transform, const glm::vec3& point)
|
||||
{
|
||||
const glm::vec3 fwd =
|
||||
EulerRotMat(transform.GetRotation()) * glm::vec3(0, 0, -1);
|
||||
glm::mat3(transform.GetRotationQuat()) * glm::vec3(0, 0, -1);
|
||||
m_OrbitPivot = point;
|
||||
m_HasPivot = true;
|
||||
transform.SetPosition(point - fwd * m_OrbitDistance);
|
||||
@@ -162,16 +158,34 @@ namespace Engine {
|
||||
return glm::inverse(t.World());
|
||||
}
|
||||
|
||||
glm::mat4 Camera::GetProjection(const f32 aspect, const f32 nearZ, const f32 farZ) const
|
||||
glm::mat4 Camera::GetProjection(const f32 aspect) const
|
||||
{
|
||||
auto proj = glm::perspective(glm::radians(45.0f), aspect, nearZ, farZ);
|
||||
glm::mat4 proj;
|
||||
if (m_ProjectionMode == ProjectionMode::Perspective)
|
||||
{
|
||||
proj = glm::perspective(glm::radians(m_FovDeg), aspect,
|
||||
m_NearZ, m_FarZ);
|
||||
proj[1][1] *= -1.0f; // Vulkan Y 翻转
|
||||
|
||||
// Vulkan clip 深度 [0,1]:z' = 0.5*z_clip + 0.5*w_clip。
|
||||
// perspective 的 w_clip = -z_eye,偏移折算进 z 系数
|
||||
//(注意:与 ortho 的公式不同,ortho 的 w=1 才能直接加到 [3][2])
|
||||
proj[2][2] = proj[2][2] * 0.5f - 0.5f;
|
||||
proj[3][2] *= 0.5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 正交(Unity orthographic size 语义 = 半垂直高度):全宽由
|
||||
// 视口 aspect 推导,长宽比跟随视口永不变形
|
||||
const f32 halfH = m_OrthoSize;
|
||||
const f32 halfW = halfH * aspect;
|
||||
proj = glm::ortho(-halfW, halfW, -halfH, halfH, m_NearZ, m_FarZ);
|
||||
proj[1][1] *= -1.0f; // Vulkan Y 翻转(与透视同款)
|
||||
|
||||
// ortho 的 w_clip = 1:偏移直接加进平移分量
|
||||
//(与 ComputeCascades 光源正交投影同一公式)
|
||||
proj[2][2] *= 0.5f;
|
||||
proj[3][2] = proj[3][2] * 0.5f + 0.5f;
|
||||
}
|
||||
return proj;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,16 @@
|
||||
#include <Engine/Core/FrameContext.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace Engine
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// 材质出生唯一号:预览 tile 池的 ABA 防线(见 Material.h)
|
||||
std::atomic<u64> s_NextMaterialSerial{1};
|
||||
}
|
||||
|
||||
Material::Material(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout layout,
|
||||
@@ -21,6 +29,7 @@ namespace Engine
|
||||
bool transparent)
|
||||
: m_Textures(textures)
|
||||
, m_Transparent(transparent)
|
||||
, m_Serial(s_NextMaterialSerial.fetch_add(1, std::memory_order_relaxed))
|
||||
{
|
||||
m_DescriptorSet = descriptorManager.AllocateDescriptorSet(layout);
|
||||
|
||||
@@ -45,6 +54,16 @@ namespace Engine
|
||||
|
||||
Material::~Material() = default;
|
||||
|
||||
// 编辑器调参:全 in-flight 槽同步覆盖写(语义与 ctor 初始化一致)。
|
||||
// 实现外置因 UniformBuffer 在 Material.h 仅前置声明
|
||||
void Material::UpdateParams(const MaterialParams& params)
|
||||
{
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_ParamsBuffer->Update(slot, ¶ms, sizeof(params));
|
||||
}
|
||||
++m_EditVersion; // 预览 tile 池的失效信号(editor 缓存重渲判据)
|
||||
}
|
||||
|
||||
std::string Material::MakeCacheKey(const std::string paths[kSlotCount],
|
||||
const MaterialParams& params,
|
||||
bool transparent)
|
||||
@@ -61,7 +80,24 @@ namespace Engine
|
||||
",c:" + std::to_string(params.baseColor.x) +
|
||||
"," + std::to_string(params.baseColor.y) +
|
||||
"," + std::to_string(params.baseColor.z) +
|
||||
"," + std::to_string(params.baseColor.w);
|
||||
"," + std::to_string(params.baseColor.w) +
|
||||
",s:" + std::to_string(params.surface.x) +
|
||||
"," + std::to_string(params.surface.y) +
|
||||
"," + std::to_string(params.surface.z) +
|
||||
"," + std::to_string(params.surface.w);
|
||||
// per-slot UV 变换 + unlit 入键:同一张贴图配不同变换是不同材质
|
||||
//(KHR_texture_transform 测试模型:一张棋盘格被 82 个材质以不同
|
||||
// 变换共享,键漏了会串成同一份)
|
||||
key += ",m:" + std::to_string(params.misc.x);
|
||||
for (u32 slot = 0; slot < kSlotCount; ++slot) {
|
||||
key += ",u" + std::to_string(slot) + ":" +
|
||||
std::to_string(params.uvA[slot].x) + "," +
|
||||
std::to_string(params.uvA[slot].y) + "," +
|
||||
std::to_string(params.uvA[slot].z) + "," +
|
||||
std::to_string(params.uvA[slot].w) + "," +
|
||||
std::to_string(params.uvT[slot].x) + "," +
|
||||
std::to_string(params.uvT[slot].y);
|
||||
}
|
||||
key += transparent ? ":t" : ":o"; // 透明语义入键(描述符相同但队列不同)
|
||||
return key;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/10/5.
|
||||
//
|
||||
|
||||
#include <Engine/Renderer/MaterialAsset.h>
|
||||
#include <Engine/Vulkan/Texture.h>
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||
|
||||
#include <yaml-cpp/yaml.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
namespace Engine
|
||||
{
|
||||
Scope<Texture> CreateDefaultMaterialTexture(
|
||||
Device& device, MemoryAllocator& allocator, Material::Slot slot)
|
||||
{
|
||||
// 与 Model::InitDefaultTextures 同语义(单点实现,两处共用);
|
||||
// 颜色槽 sRGB / 数据槽线性
|
||||
switch (slot) {
|
||||
case Material::Albedo:
|
||||
return Texture::CreateSolidColor(device, allocator, 255, 255, 255);
|
||||
case Material::Normal:
|
||||
return Texture::CreateSolidColor(device, allocator,
|
||||
128, 128, 255, false);
|
||||
case Material::MetallicRoughness:
|
||||
return Texture::CreateSolidColor(device, allocator,
|
||||
0, 128, 0, false);
|
||||
case Material::Emissive:
|
||||
return Texture::CreateSolidColor(device, allocator, 0, 0, 0);
|
||||
case Material::Ao:
|
||||
return Texture::CreateSolidColor(device, allocator,
|
||||
255, 0, 0, false);
|
||||
case Material::Roughness:
|
||||
return Texture::CreateSolidColor(device, allocator,
|
||||
128, 128, 128, false);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
// 槽位 ↔ YAML 键名(下标 = Material::Slot;改键名 = 破坏旧文件,勿动)
|
||||
constexpr const char* kSlotKeys[Material::kSlotCount] = {
|
||||
"Albedo", "Normal", "MetallicRoughness",
|
||||
"Emissive", "Ao", "Roughness"
|
||||
};
|
||||
|
||||
void EmitVec4(YAML::Emitter& e, const glm::vec4& v)
|
||||
{
|
||||
e << YAML::Flow << YAML::BeginSeq << v.x << v.y << v.z << v.w
|
||||
<< YAML::EndSeq;
|
||||
}
|
||||
|
||||
glm::vec4 ReadVec4(const YAML::Node& n, const glm::vec4& def)
|
||||
{
|
||||
if (!n || !n.IsSequence() || n.size() < 4) return def;
|
||||
return {n[0].as<f32>(def.x), n[1].as<f32>(def.y),
|
||||
n[2].as<f32>(def.z), n[3].as<f32>(def.w)};
|
||||
}
|
||||
}
|
||||
|
||||
bool MaterialAsset::Save(const std::string& path,
|
||||
const MaterialAssetData& data)
|
||||
{
|
||||
YAML::Emitter out;
|
||||
out << YAML::BeginMap;
|
||||
out << YAML::Key << "Material" << YAML::Value << YAML::BeginMap;
|
||||
out << YAML::Key << "Version" << YAML::Value << kVersion;
|
||||
|
||||
out << YAML::Key << "Textures" << YAML::Value << YAML::BeginMap;
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
out << YAML::Key << kSlotKeys[slot]
|
||||
<< YAML::Value << data.texPaths[slot];
|
||||
}
|
||||
out << YAML::EndMap; // Textures
|
||||
|
||||
// 因子块:flow 数组按 MaterialParams 分量序(shader UBO 同序)
|
||||
out << YAML::Key << "Factors" << YAML::Value;
|
||||
EmitVec4(out, data.params.factors);
|
||||
out << YAML::Key << "BaseColor" << YAML::Value;
|
||||
EmitVec4(out, data.params.baseColor);
|
||||
out << YAML::Key << "Surface" << YAML::Value;
|
||||
EmitVec4(out, data.params.surface);
|
||||
out << YAML::Key << "Misc" << YAML::Value;
|
||||
EmitVec4(out, data.params.misc);
|
||||
// per-slot UV 变换(KHR_texture_transform):6 槽各 A(4 分量) + T
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
out << YAML::Key << ("UvA" + std::to_string(slot)) << YAML::Value;
|
||||
EmitVec4(out, data.params.uvA[slot]);
|
||||
out << YAML::Key << ("UvT" + std::to_string(slot)) << YAML::Value;
|
||||
EmitVec4(out, data.params.uvT[slot]);
|
||||
}
|
||||
out << YAML::Key << "Transparent" << YAML::Value << data.transparent;
|
||||
out << YAML::EndMap; // Material
|
||||
out << YAML::EndMap;
|
||||
|
||||
std::ofstream file(path);
|
||||
if (!file.is_open()) {
|
||||
ENGINE_ERROR("MaterialAsset::Save: cannot open '{}'", path);
|
||||
return false;
|
||||
}
|
||||
file << out.c_str();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MaterialAsset::Load(const std::string& path, MaterialAssetData& out)
|
||||
{
|
||||
YAML::Node root;
|
||||
try {
|
||||
root = YAML::LoadFile(path);
|
||||
}
|
||||
catch (const YAML::Exception& e) {
|
||||
ENGINE_ERROR("MaterialAsset::Load: parse failed '{}': {}", path,
|
||||
e.what());
|
||||
return false;
|
||||
}
|
||||
const YAML::Node m = root["Material"];
|
||||
if (!m) {
|
||||
ENGINE_ERROR("MaterialAsset::Load: missing 'Material' root '{}'",
|
||||
path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Version 向前兼容:当前只认 1;未来新字段全部走缺省键兼容
|
||||
//(as<T>(default)),旧文件永远可读
|
||||
const u32 version = m["Version"].as<u32>(kVersion);
|
||||
if (version > kVersion) {
|
||||
ENGINE_WARN("MaterialAsset::Load: '{}' version {} > {} "
|
||||
"(newer file, missing fields fall back to defaults)",
|
||||
path, version, kVersion);
|
||||
}
|
||||
|
||||
if (const YAML::Node t = m["Textures"]) {
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
out.texPaths[slot] = t[kSlotKeys[slot]].as<std::string>("");
|
||||
}
|
||||
}
|
||||
// 分量序与 Save 一致;缺块 = MaterialParams 缺省值
|
||||
out.params.factors = ReadVec4(
|
||||
m["Factors"], MaterialParams{}.factors);
|
||||
out.params.baseColor = ReadVec4(
|
||||
m["BaseColor"], MaterialParams{}.baseColor);
|
||||
out.params.surface = ReadVec4(
|
||||
m["Surface"], MaterialParams{}.surface);
|
||||
out.params.misc = ReadVec4(
|
||||
m["Misc"], MaterialParams{}.misc);
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
out.params.uvA[slot] = ReadVec4(
|
||||
m["UvA" + std::to_string(slot)],
|
||||
MaterialParams{}.uvA[slot]);
|
||||
out.params.uvT[slot] = ReadVec4(
|
||||
m["UvT" + std::to_string(slot)],
|
||||
MaterialParams{}.uvT[slot]);
|
||||
}
|
||||
out.transparent = m["Transparent"].as<bool>(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
MaterialAsset::MaterialAsset(
|
||||
MaterialAssetData&& data,
|
||||
std::array<Scope<Texture>, Material::kSlotCount>&& ownedTextures,
|
||||
Scope<Material>&& material)
|
||||
: m_Data(std::move(data))
|
||||
, m_OwnedTextures(std::move(ownedTextures))
|
||||
, m_Material(std::move(material))
|
||||
{
|
||||
}
|
||||
|
||||
MaterialAsset::~MaterialAsset() = default;
|
||||
|
||||
void MaterialAsset::EditParams(const MaterialParams& params)
|
||||
{
|
||||
m_Data.params = params;
|
||||
m_Material->UpdateParams(params);
|
||||
}
|
||||
|
||||
void MaterialAsset::SetTransparent(bool t)
|
||||
{
|
||||
m_Data.transparent = t;
|
||||
m_Material->SetTransparent(t);
|
||||
}
|
||||
} // namespace Engine
|
||||
+176
-18
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Renderer/Material.h>
|
||||
#include <Engine/Renderer/MaterialAsset.h> // CreateDefaultMaterialTexture
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
@@ -78,21 +79,12 @@ void Model::RebuildUnionBounds()
|
||||
|
||||
void Model::InitDefaultTextures()
|
||||
{
|
||||
// 各材质槽位的兜底纹理(1x1,开销可忽略):
|
||||
// albedo 白 / MR 平参数(roughness 0.5, metallic 0)/ emissive 黑
|
||||
m_DefaultTextures[Material::Albedo] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 255, 255, 255);
|
||||
m_DefaultTextures[Material::MetallicRoughness] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 128, 0, false);
|
||||
m_DefaultTextures[Material::Normal] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 128, 128, 255, false);
|
||||
m_DefaultTextures[Material::Emissive] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0);
|
||||
m_DefaultTextures[Material::Ao] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 255, 0, 0, false);
|
||||
// 独立粗糙度槽兜底:0.5 灰(无图资产 = 中性粗糙度)
|
||||
m_DefaultTextures[Material::Roughness] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 128, 128, 128, false);
|
||||
// 各槽位兜底纹理(1x1,开销可忽略):语义单点维护在
|
||||
// CreateDefaultMaterialTexture(MaterialAsset 构建端共用同一组)
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
m_DefaultTextures[slot] = CreateDefaultMaterialTexture(
|
||||
m_Device, m_Allocator, static_cast<Material::Slot>(slot));
|
||||
}
|
||||
}
|
||||
|
||||
Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
@@ -139,6 +131,10 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
|
||||
m_Meshes.push_back(CreateScope<Mesh>(
|
||||
m_Device, m_Allocator, vertices, lodIndices, mat));
|
||||
// 程序化平面同样登记内建材质快照(与 m_Meshes 下标对齐,
|
||||
// 编辑器材质槽 UI 不越界)
|
||||
m_MeshMaterialInfos.push_back(
|
||||
{"Plane", {}, MaterialParams{}, transparent});
|
||||
RebuildUnionBounds();
|
||||
}
|
||||
|
||||
@@ -188,6 +184,12 @@ ParsedModel Model::ParseFromDisk(const std::string& path)
|
||||
const aiScene* scene = importer.ReadFile(path,
|
||||
aiProcess_Triangulate |
|
||||
(out.isGltf ? 0 : aiProcess_FlipUVs) |
|
||||
// 注意:不用 aiProcess_TransformUVCoords 烘焙 KHR_texture_transform
|
||||
// ——该步骤的复合顺序是"先平移后缩放"(TextureTransform.cpp
|
||||
// matrix = m2·m4·R·m3·m5,m5 最右先作用),与 glTF2Importer 写出
|
||||
// AI_MATKEY_UVTRANSFORM 时假定的"先缩放旋转后平移"不一致,
|
||||
// 顺序差在 scale≠1 时结果偏出整数倍 tile(ChronographWatch
|
||||
// 背板实测)。改由 ProcessMesh 按正确约定自烘焙
|
||||
aiProcess_GenSmoothNormals |
|
||||
aiProcess_CalcTangentSpace | // 法线贴图 TBN
|
||||
aiProcess_JoinIdenticalVertices |
|
||||
@@ -218,8 +220,11 @@ ParsedModel Model::ParseFromDisk(const std::string& path)
|
||||
std::filesystem::path(path).parent_path();
|
||||
// 骨骼注册表(boneName → palette slot):多 mesh 共享骨架去重
|
||||
std::unordered_map<std::string, u32> boneSlots;
|
||||
// GLB 内嵌贴图字节池("*N" → PNG/JPEG 原始数据)
|
||||
std::unordered_map<std::string, std::vector<u8>> embeddedImages;
|
||||
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf,
|
||||
out.bones, boneSlots);
|
||||
out.bones, boneSlots, embeddedImages);
|
||||
out.embeddedImages = std::move(embeddedImages);
|
||||
|
||||
// 骨骼 nodeIdx 回填(注册发生在各 mesh 的 ProcessMesh 内,
|
||||
// 这里按 node 表查 name 定位累乘路径)
|
||||
@@ -413,10 +418,13 @@ Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
|
||||
model->m_Nodes = std::move(parsed.nodes);
|
||||
model->m_Bones = std::move(parsed.bones);
|
||||
model->m_Clips = std::move(parsed.clips);
|
||||
model->m_EmbeddedImages = std::move(parsed.embeddedImages);
|
||||
|
||||
// 材质缓存去重 + 纹理上传 + mesh 缓冲(全部 GPU,主线程)
|
||||
for (auto& pm : parsed.meshes) {
|
||||
Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
||||
model->m_MeshMaterialInfos.push_back(
|
||||
{pm.name, pm.texPaths, pm.params, pm.transparent});
|
||||
model->m_Meshes.push_back(CreateScope<Mesh>(
|
||||
device, allocator, pm.vertices, pm.lods, mat,
|
||||
pm.cpuPositions.empty() ? nullptr : &pm.cpuPositions));
|
||||
@@ -439,8 +447,11 @@ void Model::LoadModel(const std::string& path)
|
||||
m_Nodes = std::move(parsed.nodes);
|
||||
m_Bones = std::move(parsed.bones);
|
||||
m_Clips = std::move(parsed.clips);
|
||||
m_EmbeddedImages = std::move(parsed.embeddedImages);
|
||||
for (auto& pm : parsed.meshes) {
|
||||
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
||||
m_MeshMaterialInfos.push_back(
|
||||
{pm.name, pm.texPaths, pm.params, pm.transparent});
|
||||
m_Meshes.push_back(CreateScope<Mesh>(
|
||||
m_Device, m_Allocator, pm.vertices, pm.lods, mat,
|
||||
pm.cpuPositions.empty() ? nullptr : &pm.cpuPositions));
|
||||
@@ -455,6 +466,8 @@ void Model::ProcessNode(const aiNode* node, const aiScene* scene,
|
||||
const std::filesystem::path& modelDir, bool isGltf,
|
||||
std::vector<ModelBone>& bones,
|
||||
std::unordered_map<std::string, u32>& boneSlots,
|
||||
std::unordered_map<std::string, std::vector<u8>>&
|
||||
embeddedImages,
|
||||
const aiMatrix4x4& parentGlobal)
|
||||
{
|
||||
// 累乘节点链全局变换:Blender/Sketchfab 导出的 FBX(未勾 Apply
|
||||
@@ -465,11 +478,11 @@ void Model::ProcessNode(const aiNode* node, const aiScene* scene,
|
||||
for (u32 i = 0; i < node->mNumMeshes; ++i) {
|
||||
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
|
||||
out.push_back(ProcessMesh(mesh, scene, modelDir, isGltf,
|
||||
bones, boneSlots, global));
|
||||
bones, boneSlots, embeddedImages, global));
|
||||
}
|
||||
for (u32 i = 0; i < node->mNumChildren; ++i) {
|
||||
ProcessNode(node->mChildren[i], scene, out, modelDir, isGltf,
|
||||
bones, boneSlots, global);
|
||||
bones, boneSlots, embeddedImages, global);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,6 +499,26 @@ Texture* Model::GetOrLoadTexture(const std::string& rawPath, bool srgb)
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
// GLB 内嵌贴图("*N"):字节在解析期随 ParsedModel 带过来,
|
||||
// 此处(主线程、GPU 上下文)解码上传
|
||||
if (!path.empty() && path[0] == '*') {
|
||||
const auto emb = m_EmbeddedImages.find(path);
|
||||
if (emb == m_EmbeddedImages.end()) {
|
||||
ENGINE_WARN("Embedded texture bytes not carried: {}", path);
|
||||
return nullptr;
|
||||
}
|
||||
auto tex = Texture::CreateFromEncodedMemory(
|
||||
m_Device, m_Allocator, emb->second.data(),
|
||||
static_cast<u32>(emb->second.size()), srgb);
|
||||
if (!tex) {
|
||||
ENGINE_WARN("Embedded texture decode failed: {}", path);
|
||||
return nullptr;
|
||||
}
|
||||
Texture* ptr = tex.get();
|
||||
m_TextureCache.emplace(key, std::move(tex));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
// 文件不存在则用兜底纹理
|
||||
if (!std::filesystem::exists(path)) {
|
||||
ENGINE_WARN("Texture not found, using default: {}", path);
|
||||
@@ -549,6 +582,8 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
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)
|
||||
{
|
||||
ParsedMesh out;
|
||||
@@ -626,10 +661,21 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
// 读取材质纹理路径(PBR metallic-roughness 槽位)+ 金属度/粗糙度因子
|
||||
std::string texPaths[Material::kSlotCount];
|
||||
MaterialParams params;
|
||||
std::string materialName; // 材质资产名(UI 材质槽显示/覆盖文件命名优先用它)
|
||||
|
||||
if (mesh->mMaterialIndex >= 0) {
|
||||
const aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
|
||||
|
||||
// 材质名(glTF material name / FBX 材质名):Blender 导出的 mesh
|
||||
// 名是 "Plane.023" 这类几何拓扑名,对"这是哪个材质"毫无信息量;
|
||||
// 材质名才是 UE Material Slots 列表的语义
|
||||
aiString matName;
|
||||
if (material &&
|
||||
material->Get(AI_MATKEY_NAME, matName) == aiReturn_SUCCESS &&
|
||||
matName.length > 0) {
|
||||
materialName = matName.C_Str();
|
||||
}
|
||||
|
||||
// glTF2 贴图在 assimp 5.x 的落点:MR 合图 → METALNESS(15)/
|
||||
// DIFFUSE_ROUGHNESS(16)(同图双槽);baseColor → BASE_COLOR(12)。
|
||||
// OBJ:map_Ns → SHININESS(独立 roughness 图)、map_Ka 常绑 AO。
|
||||
@@ -653,14 +699,48 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
{aiTextureType_EMISSION_COLOR, Material::Emissive},
|
||||
};
|
||||
|
||||
// 每槽命中源类型:KHR_texture_transform 在 assimp 侧按
|
||||
// aiTextureType 键控(AI_MATKEY_UVTRANSFORM(type, 0)),必须按
|
||||
// 命中时的同款类型查询才能取到对应贴图的变换
|
||||
aiTextureType slotSrcType[Material::kSlotCount] = {};
|
||||
|
||||
for (const auto& [type, slot] : slots) {
|
||||
if (!texPaths[slot].empty()) continue; // 高优先级已命中
|
||||
aiString path;
|
||||
if (material->GetTexture(type, 0, &path) == AI_SUCCESS &&
|
||||
path.length > 0) {
|
||||
slotSrcType[slot] = type;
|
||||
if (path.C_Str()[0] == '*') {
|
||||
// GLB/gltf 内嵌贴图:assimp 记作 "*<index>"(aiTexture
|
||||
// 表下标),不是磁盘路径——解析期拷出原始编码字节,
|
||||
// 实例期 GetOrLoadTexture 解码上传(解析线程无 GPU)
|
||||
texPaths[slot] = path.C_Str();
|
||||
if (!embeddedImages.count(path.C_Str())) {
|
||||
const int texIdx = atoi(path.C_Str() + 1);
|
||||
if (texIdx < 0 ||
|
||||
texIdx >= static_cast<int>(scene->mNumTextures)) {
|
||||
ENGINE_WARN("Embedded texture index out of range: "
|
||||
"{}", path.C_Str());
|
||||
continue;
|
||||
}
|
||||
const aiTexture* t = scene->mTextures[texIdx];
|
||||
if (t->mHeight != 0) {
|
||||
// 未压缩 ARGB8888 内嵌数据(罕见):折默认纹理
|
||||
ENGINE_WARN("Uncompressed embedded texture "
|
||||
"unsupported: {}", path.C_Str());
|
||||
continue;
|
||||
}
|
||||
const u8* src =
|
||||
reinterpret_cast<const u8*>(t->pcData);
|
||||
embeddedImages[path.C_Str()].assign(
|
||||
src, src + t->mWidth);
|
||||
}
|
||||
}
|
||||
else {
|
||||
texPaths[slot] = (modelDir / path.C_Str()).string();
|
||||
}
|
||||
}
|
||||
}
|
||||
// 注:不做文件名探测/自动补槽——资产缺什么就是什么;
|
||||
// 补槽是将来手动材质编辑的职责,加载期保持确定性
|
||||
|
||||
@@ -717,6 +797,81 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
params.factors.w = 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// 表面扩展四键(assimp 仅在扩展存在时写出,缺省 = 物理默认):
|
||||
// KHR_materials_ior → REFRACTI(缺省 1.5)
|
||||
// KHR_materials_transmission → TRANSMISSION_FACTOR(缺省 0)
|
||||
// KHR_materials_emissive_strength → EMISSIVE_INTENSITY(缺省 1)
|
||||
// KHR_materials_volume → VOLUME_THICKNESS_FACTOR(缺省 0)
|
||||
ai_real ior = 1.5f;
|
||||
material->Get(AI_MATKEY_REFRACTI, ior);
|
||||
ai_real transmission = 0.0f;
|
||||
material->Get(AI_MATKEY_TRANSMISSION_FACTOR, transmission);
|
||||
ai_real emissiveStrength = 1.0f;
|
||||
material->Get(AI_MATKEY_EMISSIVE_INTENSITY, emissiveStrength);
|
||||
ai_real thickness = 0.0f;
|
||||
material->Get(AI_MATKEY_VOLUME_THICKNESS_FACTOR, thickness);
|
||||
params.surface = glm::vec4(static_cast<f32>(ior),
|
||||
static_cast<f32>(transmission),
|
||||
static_cast<f32>(emissiveStrength),
|
||||
static_cast<f32>(thickness));
|
||||
|
||||
// transmission > 0 的材质必须走透明队列:物理透射表面要采样
|
||||
// 不透明场景副本做折射。Khronos 新式玻璃写法是 alphaMode=OPAQUE
|
||||
// + transmission=1(透明度语义全在 transmission,alpha 恒 1)——
|
||||
// 只认 alphaMode=BLEND 会把玻璃送进不透明队列,画成白 albedo
|
||||
// 镜面实心盖板(表盘/指针全被挡住,正面"糊成一坨")
|
||||
if (static_cast<f32>(transmission) > 0.0f) {
|
||||
out.transparent = true;
|
||||
}
|
||||
|
||||
// KHR_texture_transform:每张贴图各带独立 offset/scale/rotation
|
||||
//(AnimationPointerUVs 一致性模型:82 材质共享一张棋盘格、
|
||||
// 各槽变换各异),烘焙进顶点 UV 不可行(顶点 UV 只有一份),
|
||||
// 改写入材质参数、shader 采样前逐槽应用。
|
||||
//
|
||||
// importer 把 glTF 参数换算进 assimp 约定(缩放 → 绕 (0.5,0.5)
|
||||
// 旋转 -rot → 平移)写入 UVTRANSFORM;该换算在 θ≠0 时与 glTF
|
||||
// 规范语义不符(数值实测误差 0.44+),θ=0 才精确。这里先从
|
||||
// assimp 值精确反演回原始 glTF 参数,再用规范公式写入:
|
||||
// rot = -mRotation
|
||||
// off.x = mTrl.x - 0.5·sx·(-c + s + 1)
|
||||
// off.y = 1 - sy - mTrl.y + 0.5·sy·(s + c - 1)
|
||||
// spec(glTF v-down 空间 uv' = T(off)·R(rot)·S(scale)·uv,
|
||||
// 折算进引擎 v-up 顶点 UV 空间 uv' = A·uv + t):
|
||||
// A = [[c·sx, s·sy], [-s·sx, c·sy]](行主序摊平 vec4)
|
||||
// t = (off.x - s·sy, 1 - off.y - c·sy)
|
||||
// rot=0 时退化为已视觉验证的旧烘焙式 t=(off.x, 1-sy-off.y)
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
if (texPaths[slot].empty()) continue;
|
||||
aiUVTransform at;
|
||||
if (material->Get(AI_MATKEY_UVTRANSFORM(slotSrcType[slot], 0),
|
||||
at) != AI_SUCCESS) {
|
||||
continue;
|
||||
}
|
||||
const f32 sx = static_cast<f32>(at.mScaling.x);
|
||||
const f32 sy = static_cast<f32>(at.mScaling.y);
|
||||
const f32 rot = -static_cast<f32>(at.mRotation);
|
||||
const f32 c = std::cos(rot);
|
||||
const f32 s = std::sin(rot);
|
||||
const f32 offx = static_cast<f32>(at.mTranslation.x)
|
||||
- 0.5f * sx * (-c + s + 1.0f);
|
||||
const f32 offy = 1.0f - sy
|
||||
- static_cast<f32>(at.mTranslation.y)
|
||||
+ 0.5f * sy * (s + c - 1.0f);
|
||||
params.uvA[slot] = glm::vec4(c * sx, s * sy, -s * sx, c * sy);
|
||||
params.uvT[slot] = glm::vec4(offx - s * sy,
|
||||
1.0f - offy - c * sy,
|
||||
0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// KHR_materials_unlit:misc.x=1 → shader 早退直出 albedo
|
||||
int shadingModel = 0;
|
||||
if (material->Get(AI_MATKEY_SHADING_MODEL, shadingModel)
|
||||
== AI_SUCCESS &&
|
||||
shadingModel == aiShadingMode_Unlit) {
|
||||
params.misc.x = 1.0f;
|
||||
}
|
||||
} else {
|
||||
// FBX/OBJ 独立贴图工作流
|
||||
const bool hasSepRough = !texPaths[Material::Roughness].empty();
|
||||
@@ -745,6 +900,9 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
}
|
||||
out.params = params;
|
||||
// out.transparent 已在 glTF 分支读取
|
||||
// UI 名字:材质名优先(语义对齐 UE Material Slots),无材质名
|
||||
//(罕见)回退 mesh 名
|
||||
out.name = !materialName.empty() ? materialName : mesh->mName.C_Str();
|
||||
|
||||
// ---- LOD 链(解析期生成,worker 线程免费时间):----
|
||||
// LOD0 = 原始;每级目标索引数减半,相对误差 1e-2。顶点共享,
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
//
|
||||
// EditorGizmoPass.cpp:pass 装配(shader object)与录制。
|
||||
// editor 专属(HELLO_EDITOR):game 构建整 TU 剔除
|
||||
//(Engine/CMakeLists REMOVE_ITEM)。
|
||||
//
|
||||
|
||||
#include <Engine/Renderer/Passes/EditorGizmoPass.h>
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
#include <Engine/Vulkan/UniformBuffer.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
|
||||
#include <tracy/Tracy.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace Engine::Passes
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// 线流布局:与 debug_line 同(location0 pos / location1 color)。
|
||||
// setVertexInputEXT 走 2EXT 结构(0 divisor 需 feature,不用)
|
||||
vk::VertexInputBindingDescription2EXT MakeWireBinding()
|
||||
{
|
||||
vk::VertexInputBindingDescription2EXT b;
|
||||
b.setBinding(0)
|
||||
.setStride(sizeof(GizmoWireVertex))
|
||||
.setInputRate(vk::VertexInputRate::eVertex)
|
||||
.setDivisor(1);
|
||||
return b;
|
||||
}
|
||||
|
||||
std::array<vk::VertexInputAttributeDescription2EXT, 2>
|
||||
MakeWireAttributes()
|
||||
{
|
||||
std::array<vk::VertexInputAttributeDescription2EXT, 2> a;
|
||||
a[0].setLocation(0).setBinding(0)
|
||||
.setFormat(vk::Format::eR32G32B32Sfloat)
|
||||
.setOffset(offsetof(GizmoWireVertex, pos));
|
||||
a[1].setLocation(1).setBinding(0)
|
||||
.setFormat(vk::Format::eR32G32B32A32Sfloat)
|
||||
.setOffset(offsetof(GizmoWireVertex, color));
|
||||
return a;
|
||||
}
|
||||
|
||||
// 图标流布局:pos / color / aux(shapeId + uv)。entityId 槽位
|
||||
// 本 pass 不消费——拾取由 HitProxyPass 用同一 VB 全四属性画
|
||||
vk::VertexInputBindingDescription2EXT MakeIconBinding()
|
||||
{
|
||||
vk::VertexInputBindingDescription2EXT b;
|
||||
b.setBinding(0)
|
||||
.setStride(sizeof(GizmoQuadVertex))
|
||||
.setInputRate(vk::VertexInputRate::eVertex)
|
||||
.setDivisor(1);
|
||||
return b;
|
||||
}
|
||||
|
||||
std::array<vk::VertexInputAttributeDescription2EXT, 3>
|
||||
MakeIconAttributes()
|
||||
{
|
||||
std::array<vk::VertexInputAttributeDescription2EXT, 3> a;
|
||||
a[0].setLocation(0).setBinding(0)
|
||||
.setFormat(vk::Format::eR32G32B32Sfloat)
|
||||
.setOffset(offsetof(GizmoQuadVertex, pos));
|
||||
a[1].setLocation(1).setBinding(0)
|
||||
.setFormat(vk::Format::eR32G32B32A32Sfloat)
|
||||
.setOffset(offsetof(GizmoQuadVertex, color));
|
||||
a[2].setLocation(2).setBinding(0)
|
||||
.setFormat(vk::Format::eR32G32B32A32Sfloat)
|
||||
.setOffset(offsetof(GizmoQuadVertex, aux));
|
||||
return a;
|
||||
}
|
||||
|
||||
// 直写方程(线流):(ONE, ZERO) → dst = src,数学上等价无混合
|
||||
vk::ColorBlendEquationEXT MakeDirectEquation()
|
||||
{
|
||||
vk::ColorBlendEquationEXT eq;
|
||||
eq.setSrcColorBlendFactor(vk::BlendFactor::eOne)
|
||||
.setDstColorBlendFactor(vk::BlendFactor::eZero)
|
||||
.setColorBlendOp(vk::BlendOp::eAdd)
|
||||
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
|
||||
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
|
||||
.setAlphaBlendOp(vk::BlendOp::eAdd);
|
||||
return eq;
|
||||
}
|
||||
|
||||
// 预乘 alpha(图标流):shader 已输出预乘色,src 原样加权叠加
|
||||
vk::ColorBlendEquationEXT MakePremultEquation()
|
||||
{
|
||||
vk::ColorBlendEquationEXT eq;
|
||||
eq.setSrcColorBlendFactor(vk::BlendFactor::eOne)
|
||||
.setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
|
||||
.setColorBlendOp(vk::BlendOp::eAdd)
|
||||
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
|
||||
.setDstAlphaBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
|
||||
.setAlphaBlendOp(vk::BlendOp::eAdd);
|
||||
return eq;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
EditorGizmoPass::EditorGizmoPass(Device& device, ShaderManager& shaders,
|
||||
DescriptorManager& descriptors,
|
||||
FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor,
|
||||
RG::TextureHandle depth,
|
||||
UniformBuffer& iconVB,
|
||||
UniformBuffer& wireVB)
|
||||
: m_Device(device), m_Descriptors(descriptors), m_Frame(frame),
|
||||
m_HDRColor(hdrColor), m_Depth(depth), m_IconVB(iconVB),
|
||||
m_WireVB(wireVB)
|
||||
{
|
||||
// 无描述符 set:viewProj 走 push constant(两对 shader 同契约)
|
||||
m_Layout = descriptors.CreatePipelineLayout(
|
||||
std::vector<vk::DescriptorSetLayout>{}, sizeof(glm::mat4));
|
||||
|
||||
const std::string shaderDir = "shaders/";
|
||||
auto wireVert = shaders.CreateFromFile(
|
||||
shaderDir + "debug_line.vert.spv",
|
||||
vk::ShaderStageFlagBits::eVertex,
|
||||
std::vector<vk::DescriptorSetLayout>{},
|
||||
sizeof(glm::mat4));
|
||||
auto wireFrag = shaders.CreateFromFile(
|
||||
shaderDir + "debug_line.frag.spv",
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
std::vector<vk::DescriptorSetLayout>{},
|
||||
sizeof(glm::mat4));
|
||||
auto iconVert = shaders.CreateFromFile(
|
||||
shaderDir + "gizmo_billboard.vert.spv",
|
||||
vk::ShaderStageFlagBits::eVertex,
|
||||
std::vector<vk::DescriptorSetLayout>{},
|
||||
sizeof(glm::mat4));
|
||||
auto iconFrag = shaders.CreateFromFile(
|
||||
shaderDir + "gizmo_billboard.frag.spv",
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
std::vector<vk::DescriptorSetLayout>{},
|
||||
sizeof(glm::mat4));
|
||||
|
||||
m_WireVert = std::move(wireVert);
|
||||
m_WireFrag = std::move(wireFrag);
|
||||
m_IconVert = std::move(iconVert);
|
||||
m_IconFrag = std::move(iconFrag);
|
||||
if (!m_WireVert || !m_WireFrag || !m_IconVert || !m_IconFrag) {
|
||||
ENGINE_FATAL("EditorGizmoPass: failed to create shader objects");
|
||||
}
|
||||
}
|
||||
|
||||
EditorGizmoPass::~EditorGizmoPass()
|
||||
{
|
||||
// shader 随 ShaderRef 引用计数自动销毁,此处只清其余原生句柄
|
||||
m_Device.GetDevice().destroyPipelineLayout(m_Layout);
|
||||
}
|
||||
|
||||
void EditorGizmoPass::Register(RG::RenderGraph& rg)
|
||||
{
|
||||
rg.AddPass("EditorGizmo", [this](RG::RenderGraphContext& ctx) {
|
||||
const u32 iconCount = static_cast<u32>(
|
||||
m_Frame.gizmoIconVerts.size());
|
||||
const u32 wireCount = static_cast<u32>(
|
||||
m_Frame.gizmoWireVerts.size());
|
||||
// 骨骼线(选中实体骨架):与 gizmo 线同 VB 同管线,画在
|
||||
// gizmo 段之后。截断公式与 Renderer 上传端一致(同一 VB
|
||||
// 容量推导,天然对齐)
|
||||
const u64 wireVBVerts =
|
||||
m_WireVB.GetSize() / sizeof(GizmoWireVertex);
|
||||
const u64 boneRoom =
|
||||
wireVBVerts > wireCount ? wireVBVerts - wireCount : 0;
|
||||
const u32 boneCount = static_cast<u32>(std::min<u64>(
|
||||
m_Frame.boneWireVerts.size(), boneRoom));
|
||||
if (iconCount == 0 && wireCount == 0 && boneCount == 0) {
|
||||
return; // 空帧零开销
|
||||
}
|
||||
|
||||
ZoneScopedN("EditorGizmo");
|
||||
const vk::CommandBuffer cmd = ctx.cmd;
|
||||
|
||||
// HDR:Load 承接 Forward/PhysicsDebug 结果叠加;深度:Load
|
||||
// 承接场景深度做测试(只测不写),DontCare——HitProxy 稍后
|
||||
// Clear 重建
|
||||
vk::RenderingAttachmentInfo color;
|
||||
color.setImageView(ctx.View(m_HDRColor))
|
||||
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore);
|
||||
|
||||
vk::RenderingAttachmentInfo depth;
|
||||
depth.setImageView(ctx.View(m_Depth))
|
||||
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eDontCare);
|
||||
|
||||
vk::RenderingInfo renderingInfo;
|
||||
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
|
||||
.setLayerCount(1)
|
||||
.setColorAttachments(color)
|
||||
.setPDepthAttachment(&depth);
|
||||
|
||||
cmd.beginRendering(renderingInfo);
|
||||
|
||||
const std::array<vk::ShaderStageFlagBits, 2> stages = {
|
||||
vk::ShaderStageFlagBits::eVertex,
|
||||
vk::ShaderStageFlagBits::eFragment
|
||||
};
|
||||
|
||||
// layout 的 push range 是 vertex|fragment(CreatePipelineLayout
|
||||
// 默认),stageFlags 必须覆盖 range 全部 stage(VUID-01796)
|
||||
cmd.pushConstants<glm::mat4>(
|
||||
m_Layout,
|
||||
vk::ShaderStageFlagBits::eVertex |
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
0, m_Frame.ubo.proj * m_Frame.ubo.view);
|
||||
|
||||
// ---- 公共动态状态(shader object 无管线对象,无条件直写;
|
||||
// 跨 pass 残留教训:公共状态块不进条件分支)----
|
||||
cmd.setRasterizerDiscardEnable(VK_FALSE);
|
||||
|
||||
cmd.setDepthTestEnable(VK_TRUE);
|
||||
cmd.setDepthCompareOp(vk::CompareOp::eLessOrEqual);
|
||||
cmd.setDepthWriteEnable(VK_FALSE); // gizmo 不污染场景深度
|
||||
cmd.setDepthBoundsTestEnable(VK_FALSE);
|
||||
cmd.setStencilTestEnable(VK_FALSE);
|
||||
cmd.setDepthBiasEnable(VK_FALSE);
|
||||
|
||||
cmd.setPolygonModeEXT(vk::PolygonMode::eFill);
|
||||
cmd.setCullMode(vk::CullModeFlagBits::eNone);
|
||||
cmd.setFrontFace(vk::FrontFace::eCounterClockwise);
|
||||
cmd.setPrimitiveRestartEnable(VK_FALSE);
|
||||
|
||||
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
|
||||
constexpr vk::SampleMask sampleMask = 0xFFFFFFFF;
|
||||
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
|
||||
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
|
||||
cmd.setAlphaToOneEnableEXT(VK_FALSE);
|
||||
|
||||
const vk::Viewport viewport(
|
||||
0.0f, 0.0f, static_cast<float>(ctx.extent.width),
|
||||
static_cast<float>(ctx.extent.height), 0.0f, 1.0f);
|
||||
cmd.setViewportWithCount(viewport);
|
||||
cmd.setScissorWithCount(vk::Rect2D({0, 0}, ctx.extent));
|
||||
|
||||
constexpr vk::ColorComponentFlags colorWriteMask =
|
||||
vk::ColorComponentFlagBits::eR |
|
||||
vk::ColorComponentFlagBits::eG |
|
||||
vk::ColorComponentFlagBits::eB |
|
||||
vk::ColorComponentFlagBits::eA;
|
||||
cmd.setColorWriteMaskEXT(0, colorWriteMask);
|
||||
|
||||
// ---- 线流:视锥线框 + 选中实体骨骼(LineList 无混合直写,
|
||||
// 同 VB 两段:[gizmo 线 | 骨骼线])----
|
||||
if (wireCount > 0 || boneCount > 0) {
|
||||
const std::array<vk::ShaderEXT, 2> wireShaders = {
|
||||
m_WireVert.GetHandle(), m_WireFrag.GetHandle()};
|
||||
cmd.bindShadersEXT(stages, wireShaders);
|
||||
|
||||
const vk::VertexInputBindingDescription2EXT binding =
|
||||
MakeWireBinding();
|
||||
const auto attrs = MakeWireAttributes();
|
||||
cmd.setVertexInputEXT(binding, attrs);
|
||||
|
||||
cmd.setPrimitiveTopology(
|
||||
vk::PrimitiveTopology::eLineList);
|
||||
cmd.setLineWidth(1.0f); // 线拓扑必须显式(VUID-08617)
|
||||
|
||||
constexpr vk::Bool32 blendOff = VK_FALSE;
|
||||
const vk::ColorBlendEquationEXT direct =
|
||||
MakeDirectEquation();
|
||||
cmd.setColorBlendEnableEXT(0, blendOff);
|
||||
cmd.setColorBlendEquationEXT(0, direct);
|
||||
|
||||
const std::array<vk::Buffer, 1> bufs{
|
||||
m_WireVB.Get(ctx.frame.frameSlot)};
|
||||
const std::array<vk::DeviceSize, 1> offs{0};
|
||||
cmd.bindVertexBuffers(0, bufs, offs);
|
||||
if (wireCount > 0) {
|
||||
cmd.draw(wireCount, 1, 0, 0);
|
||||
}
|
||||
if (boneCount > 0) {
|
||||
cmd.draw(boneCount, 1, wireCount, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 图标流:billboard(TriangleList 预乘 alpha 叠加)----
|
||||
if (iconCount > 0) {
|
||||
const std::array<vk::ShaderEXT, 2> iconShaders = {
|
||||
m_IconVert.GetHandle(), m_IconFrag.GetHandle()};
|
||||
cmd.bindShadersEXT(stages, iconShaders);
|
||||
|
||||
const vk::VertexInputBindingDescription2EXT binding =
|
||||
MakeIconBinding();
|
||||
const auto attrs = MakeIconAttributes();
|
||||
cmd.setVertexInputEXT(binding, attrs);
|
||||
|
||||
cmd.setPrimitiveTopology(
|
||||
vk::PrimitiveTopology::eTriangleList);
|
||||
|
||||
constexpr vk::Bool32 blendOn = VK_TRUE;
|
||||
const vk::ColorBlendEquationEXT premult =
|
||||
MakePremultEquation();
|
||||
cmd.setColorBlendEnableEXT(0, blendOn);
|
||||
cmd.setColorBlendEquationEXT(0, premult);
|
||||
|
||||
const std::array<vk::Buffer, 1> bufs{
|
||||
m_IconVB.Get(ctx.frame.frameSlot)};
|
||||
const std::array<vk::DeviceSize, 1> offs{0};
|
||||
cmd.bindVertexBuffers(0, bufs, offs);
|
||||
cmd.draw(iconCount, 1, 0, 0);
|
||||
}
|
||||
|
||||
cmd.endRendering();
|
||||
})
|
||||
// Write use(WAW 链):HDR 排 Forward/PhysicsDebug 之后、TAA 之前;
|
||||
// Depth WAW 把本 pass 固定排进深度链(HitProxy 清深度之前——
|
||||
// gizmo 深度测试消费场景深度,不是上帧残留)
|
||||
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
|
||||
.Write(m_Depth, RG::ResourceUse::DepthAttachment);
|
||||
}
|
||||
|
||||
} // namespace Engine::Passes
|
||||
@@ -177,7 +177,7 @@ namespace Engine::Passes
|
||||
for (const auto& batch : m_Frame.mainView.batches) {
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 1,
|
||||
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
|
||||
batch.material->GetDescriptorSet(), {});
|
||||
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
|
||||
}
|
||||
}
|
||||
@@ -194,6 +194,30 @@ namespace Engine::Passes
|
||||
.Read(m_ShadowMaps[2], RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment);
|
||||
|
||||
// ---- 场景色副本:不透明段(含天空盒背景)的快照 ----
|
||||
// 透射折射的采样源。必须排在 Forward 之后(RAW 链)、
|
||||
// ForwardTrans 之前(后者对 HDRColor 的写入不能进折射样本,
|
||||
// 且玻璃互相不可见——屏幕空间透射只含不透明场景,与 UE 一致)。
|
||||
rg.AddPass("SceneColorCopy", [this](RG::RenderGraphContext& ctx) {
|
||||
const vk::CommandBuffer cmd = ctx.cmd;
|
||||
|
||||
vk::ImageCopy region{};
|
||||
region.srcSubresource =
|
||||
vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor,
|
||||
0, 0, 1);
|
||||
region.dstSubresource =
|
||||
vk::ImageSubresourceLayers(vk::ImageAspectFlagBits::eColor,
|
||||
0, 0, 1);
|
||||
region.setExtent(vk::Extent3D(ctx.extent.width, ctx.extent.height, 1));
|
||||
cmd.copyImage(ctx.Image(m_HDRColor),
|
||||
vk::ImageLayout::eTransferSrcOptimal,
|
||||
ctx.Image(m_SceneColor),
|
||||
vk::ImageLayout::eTransferDstOptimal,
|
||||
{region});
|
||||
})
|
||||
.Read(m_HDRColor, RG::ResourceUse::TransferSrc)
|
||||
.Write(m_SceneColor, RG::ResourceUse::TransferDst);
|
||||
|
||||
// ---- 透明段:独立渲染实例,UE/Unity 标准语义 ----
|
||||
// 深度测试对不透明深度(eLess)+ 深度写入关闭——玻璃之间
|
||||
// 互相不遮挡(谁先画都留在画面上,先后只影响 over 混合的
|
||||
@@ -280,7 +304,7 @@ namespace Engine::Passes
|
||||
for (const auto& batch : m_Frame.translucentView.batches) {
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 1,
|
||||
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
|
||||
batch.material->GetDescriptorSet(), {});
|
||||
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
|
||||
}
|
||||
|
||||
@@ -292,6 +316,9 @@ namespace Engine::Passes
|
||||
// barrier 覆盖透明段对不透明深度的测试读(Read-Write hazard
|
||||
// 由 src=Write 的内存域保证可见性)
|
||||
.Write(m_Depth, RG::ResourceUse::DepthAttachment)
|
||||
// 场景色副本:透射折射采样源(translucent frag set2 binding 6)
|
||||
.Read(m_SceneColor, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
.Read(m_ShadowMaps[0], RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
.Read(m_ShadowMaps[1], RG::ResourceUse::ShaderRead,
|
||||
@@ -300,4 +327,10 @@ namespace Engine::Passes
|
||||
vk::ShaderStageFlagBits::eFragment);
|
||||
}
|
||||
|
||||
void ForwardPass::OnTargetsRebuilt(RG::RenderGraph& rg)
|
||||
{
|
||||
// RG 物理镜像按槽位各一份:重编译后重绑场景色副本
|
||||
m_Gpu.BindSceneColor(rg, m_SceneColor);
|
||||
}
|
||||
|
||||
} // namespace Engine::Passes
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
#include <Engine/Vulkan/ShaderManager.h>
|
||||
#include <Engine/Vulkan/UniformBuffer.h> // gizmo 代理 VB 完整类型(editor)
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
@@ -42,6 +43,39 @@ namespace Engine::Passes
|
||||
vk::ColorComponentFlagBits::eG |
|
||||
vk::ColorComponentFlagBits::eB |
|
||||
vk::ColorComponentFlagBits::eA;
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
// gizmo 代理壳布局:GizmoQuadVertex 四属性流(entityId = loc3
|
||||
// R32Uint → flat varying → R32Uint 目标)
|
||||
vk::VertexInputBindingDescription2EXT MakeGizmoProxyBinding()
|
||||
{
|
||||
vk::VertexInputBindingDescription2EXT b;
|
||||
b.setBinding(0)
|
||||
.setStride(sizeof(GizmoQuadVertex))
|
||||
.setInputRate(vk::VertexInputRate::eVertex)
|
||||
.setDivisor(1);
|
||||
return b;
|
||||
}
|
||||
|
||||
std::array<vk::VertexInputAttributeDescription2EXT, 4>
|
||||
MakeGizmoProxyAttributes()
|
||||
{
|
||||
std::array<vk::VertexInputAttributeDescription2EXT, 4> a;
|
||||
a[0].setLocation(0).setBinding(0)
|
||||
.setFormat(vk::Format::eR32G32B32Sfloat)
|
||||
.setOffset(offsetof(GizmoQuadVertex, pos));
|
||||
a[1].setLocation(1).setBinding(0)
|
||||
.setFormat(vk::Format::eR32G32B32A32Sfloat)
|
||||
.setOffset(offsetof(GizmoQuadVertex, color));
|
||||
a[2].setLocation(2).setBinding(0)
|
||||
.setFormat(vk::Format::eR32G32B32A32Sfloat)
|
||||
.setOffset(offsetof(GizmoQuadVertex, aux));
|
||||
a[3].setLocation(3).setBinding(0)
|
||||
.setFormat(vk::Format::eR32Uint)
|
||||
.setOffset(offsetof(GizmoQuadVertex, entityId));
|
||||
return a;
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
HitProxyPass::HitProxyPass(Device& device, MemoryAllocator& allocator,
|
||||
@@ -79,12 +113,39 @@ namespace Engine::Passes
|
||||
vk::BufferUsageFlagBits::eTransferDst,
|
||||
vk::MemoryPropertyFlagBits::eHostVisible |
|
||||
vk::MemoryPropertyFlagBits::eHostCoherent);
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
// ---- gizmo 代理壳拾取装配(editor 专属):无描述符 layout
|
||||
// (push = viewProj)+ gizmo_pick shader 对。VB 由 Renderer
|
||||
// BuildGraph 内 AttachGizmoProxy 接入 ----
|
||||
m_GizmoLayout = descriptors.CreatePipelineLayout(
|
||||
std::vector<vk::DescriptorSetLayout>{}, sizeof(glm::mat4));
|
||||
|
||||
auto gizmoVert = shaders.CreateFromFile(
|
||||
shaderDir + "gizmo_pick.vert.spv",
|
||||
vk::ShaderStageFlagBits::eVertex,
|
||||
std::vector<vk::DescriptorSetLayout>{},
|
||||
sizeof(glm::mat4));
|
||||
auto gizmoFrag = shaders.CreateFromFile(
|
||||
shaderDir + "gizmo_pick.frag.spv",
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
std::vector<vk::DescriptorSetLayout>{},
|
||||
sizeof(glm::mat4));
|
||||
m_GizmoVert = std::move(gizmoVert);
|
||||
m_GizmoFrag = std::move(gizmoFrag);
|
||||
if (!m_GizmoVert || !m_GizmoFrag) {
|
||||
ENGINE_FATAL("HitProxyPass: failed to create gizmo pick shaders");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
HitProxyPass::~HitProxyPass()
|
||||
{
|
||||
// shader 随 ShaderRef 引用计数自动销毁,此处只清其余原生句柄
|
||||
m_Device.GetDevice().destroyPipelineLayout(m_Layout);
|
||||
#ifdef HELLO_EDITOR
|
||||
m_Device.GetDevice().destroyPipelineLayout(m_GizmoLayout);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool HitProxyPass::TakeResult(u32& outBits)
|
||||
@@ -196,6 +257,38 @@ namespace Engine::Passes
|
||||
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
|
||||
}
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
// ---- gizmo 拾取代理壳(editor 专属):billboard 图标 quad +
|
||||
// 视锥棱 ribbon 用 gizmo_pick 管线重画进 R32Uint。与 mesh
|
||||
// 同一深度解算(Less + write ON,最近者胜)——代理壳挡住
|
||||
// 的 mesh 拾不到、被 mesh 挡住的代理壳拾不到(深度测试
|
||||
// 与可见 gizmo 渲染一致,点击命中 = 所见即所得)----
|
||||
if (m_GizmoProxyVB && !m_Frame.gizmoProxyVerts.empty()) {
|
||||
const std::array<vk::ShaderEXT, 2> gizmoShaders = {
|
||||
m_GizmoVert.GetHandle(), m_GizmoFrag.GetHandle()};
|
||||
cmd.bindShadersEXT(stages, gizmoShaders);
|
||||
|
||||
const vk::VertexInputBindingDescription2EXT gizmoBinding =
|
||||
MakeGizmoProxyBinding();
|
||||
const auto gizmoAttrs = MakeGizmoProxyAttributes();
|
||||
cmd.setVertexInputEXT(gizmoBinding, gizmoAttrs);
|
||||
|
||||
// gizmo layout 无描述符:viewProj 经 push constant 下发
|
||||
cmd.pushConstants<glm::mat4>(
|
||||
m_GizmoLayout,
|
||||
vk::ShaderStageFlagBits::eVertex |
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
0, m_Frame.ubo.proj * m_Frame.ubo.view);
|
||||
|
||||
const std::array<vk::Buffer, 1> gizmoBufs{
|
||||
m_GizmoProxyVB->Get(ctx.frame.frameSlot)};
|
||||
const std::array<vk::DeviceSize, 1> gizmoOffs{0};
|
||||
cmd.bindVertexBuffers(0, gizmoBufs, gizmoOffs);
|
||||
cmd.draw(
|
||||
static_cast<u32>(m_Frame.gizmoProxyVerts.size()), 1, 0, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
cmd.endRendering();
|
||||
})
|
||||
.Write(m_IdTarget, RG::ResourceUse::ColorAttachment)
|
||||
|
||||
@@ -48,7 +48,14 @@ namespace Engine::Passes
|
||||
// 场景纹理在本 pass 内被 ImGui::Image 的描述符采样:声明读让 RG
|
||||
// 补 ColorAttachment→ShaderRead barrier,并推导 eSampled usage
|
||||
.Read(m_PresentTarget, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment);
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
#ifdef HELLO_EDITOR
|
||||
// 材质球预览 atlas 同理(材质槽行的 ImGui::Image 采样);
|
||||
// game 构建不声明(atlas 不存在)
|
||||
.Read(m_PreviewAtlas, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
#endif
|
||||
;
|
||||
}
|
||||
|
||||
} // namespace Engine::Passes
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/10/5.
|
||||
//
|
||||
|
||||
#include <Engine/Renderer/Passes/MaterialPreviewPass.h>
|
||||
#include <Engine/Renderer/Passes/SceneGpu.h>
|
||||
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
||||
#include <Engine/Renderer/Mesh.h>
|
||||
#include <Engine/Renderer/Material.h>
|
||||
#include <Engine/Renderer/Cubemap.h>
|
||||
#include <Engine/Renderer/Vertex.h>
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
#include <Engine/Vulkan/UniformBuffer.h>
|
||||
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace Engine::Passes
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr vk::ColorComponentFlags kColorWriteMask =
|
||||
vk::ColorComponentFlagBits::eR |
|
||||
vk::ColorComponentFlagBits::eG |
|
||||
vk::ColorComponentFlagBits::eB |
|
||||
vk::ColorComponentFlagBits::eA;
|
||||
|
||||
// 直写方程(与 ForwardPass 不透明段一致)
|
||||
vk::ColorBlendEquationEXT MakeDirectEquation()
|
||||
{
|
||||
vk::ColorBlendEquationEXT eq;
|
||||
eq.setSrcColorBlendFactor(vk::BlendFactor::eOne)
|
||||
.setDstColorBlendFactor(vk::BlendFactor::eZero)
|
||||
.setColorBlendOp(vk::BlendOp::eAdd)
|
||||
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
|
||||
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
|
||||
.setAlphaBlendOp(vk::BlendOp::eAdd);
|
||||
return eq;
|
||||
}
|
||||
|
||||
// 预乘 over(与 ForwardPass 透明段一致):HDR 域线性合成,
|
||||
// alpha 不动(tile 背景不透明,tonemap 段 rgb×a 恒 ×1)
|
||||
vk::ColorBlendEquationEXT MakePremulEquation()
|
||||
{
|
||||
vk::ColorBlendEquationEXT eq;
|
||||
eq.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
|
||||
.setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
|
||||
.setColorBlendOp(vk::BlendOp::eAdd)
|
||||
.setSrcAlphaBlendFactor(vk::BlendFactor::eZero)
|
||||
.setDstAlphaBlendFactor(vk::BlendFactor::eOne)
|
||||
.setAlphaBlendOp(vk::BlendOp::eAdd);
|
||||
return eq;
|
||||
}
|
||||
|
||||
// 程序化 UV 球(世界空间,原点,半径 1):32×16 分段。法线 =
|
||||
// 单位位置;无切线(frag 退回几何法线);无骨骼(默认 weights
|
||||
// 直通 palette[0] identity)。索引序经叉积校验为外向 CCW
|
||||
void BuildSphere(std::vector<Vertex>& vertices,
|
||||
std::vector<u32>& indices)
|
||||
{
|
||||
constexpr u32 kSlices = 32;
|
||||
constexpr u32 kStacks = 16;
|
||||
constexpr f32 kPi = 3.14159265358979f;
|
||||
|
||||
vertices.reserve((kStacks + 1) * (kSlices + 1));
|
||||
for (u32 j = 0; j <= kStacks; ++j) {
|
||||
const f32 phi = f32(j) / kStacks * kPi; // 0 = 顶极
|
||||
for (u32 i = 0; i <= kSlices; ++i) {
|
||||
const f32 theta = f32(i) / kSlices * 2.0f * kPi;
|
||||
const f32 sx = std::sin(phi) * std::cos(theta);
|
||||
const f32 sy = std::cos(phi);
|
||||
const f32 sz = std::sin(phi) * std::sin(theta);
|
||||
vertices.push_back(Vertex{
|
||||
{sx, sy, sz}, // position
|
||||
{sx, sy, sz}, // normal = 单位位置
|
||||
{f32(i) / kSlices, f32(j) / kStacks}});
|
||||
}
|
||||
}
|
||||
|
||||
const u32 row = kSlices + 1;
|
||||
indices.reserve(kStacks * kSlices * 6);
|
||||
for (u32 j = 0; j < kStacks; ++j) {
|
||||
for (u32 i = 0; i < kSlices; ++i) {
|
||||
const u32 v00 = j * row + i;
|
||||
const u32 v10 = v00 + 1; // θ+
|
||||
const u32 v01 = v00 + row; // φ+
|
||||
const u32 v11 = v01 + 1;
|
||||
indices.push_back(v00);
|
||||
indices.push_back(v10);
|
||||
indices.push_back(v11);
|
||||
indices.push_back(v00);
|
||||
indices.push_back(v11);
|
||||
indices.push_back(v01);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
MaterialPreviewPass::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)
|
||||
: m_Device(device)
|
||||
, m_Allocator(allocator)
|
||||
, m_Descriptors(descriptors)
|
||||
, m_Gpu(gpu)
|
||||
, m_Fullscreen(fullscreen)
|
||||
, m_HDRAtlas(hdrAtlas)
|
||||
, m_VelocityAtlas(velocityAtlas)
|
||||
, m_SDRAtlas(sdrAtlas)
|
||||
, m_DirtyTiles(dirtyTiles)
|
||||
{
|
||||
// ---- set0:棚拍 UBO + GPUDriven 契约占位(内容恒定,创建期
|
||||
// 一次写入;in-flight 帧并发只读同一份,无需 per-slot ring)----
|
||||
UniformBufferObject ubo{};
|
||||
// 相机距离 3.6:半径 1 球的角半径 atan(1/3.6)≈15.5° < fov/2 17.5°,
|
||||
// 球完整内切 tile(3.0 时球超出视锥被上下裁切)
|
||||
ubo.view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.6f),
|
||||
glm::vec3(0.0f),
|
||||
glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
ubo.proj = glm::perspective(glm::radians(35.0f), 1.0f, 0.1f, 10.0f);
|
||||
ubo.proj[1][1] *= -1.0f; // Vulkan clip Y 翻转(与主相机同语义)
|
||||
ubo.projNoJitter = ubo.proj;
|
||||
ubo.prevView = ubo.view; // 静止球:velocity 恒 0
|
||||
ubo.prevProj = ubo.proj;
|
||||
ubo.ambientColor = glm::vec4(1.0f); // IBL 强度乘子 = 全量
|
||||
ubo.cascadeSplits = glm::vec4(0.0f); // w=0 → frag 阴影短路(不采阴影图)
|
||||
ubo.cameraPos = glm::vec4(0.0f, 0.0f, 3.6f, 1.0f);
|
||||
// 固定棚拍主光:暖白方向光(从左上前方打向球体)。灯光 1-3 留零
|
||||
//(color.a=0 → 辐射为零)
|
||||
ubo.lights[0].color = glm::vec4(1.0f, 0.98f, 0.94f, 2.2f);
|
||||
ubo.lights[0].direction = glm::vec4(
|
||||
glm::normalize(glm::vec3(-0.45f, -0.7f, -0.55f)), 0.0f);
|
||||
ubo.lights[0].params = glm::vec4(0.0f); // x=0 → 方向光
|
||||
|
||||
m_PreviewUbo = CreateScope<UniformBuffer>(
|
||||
device, allocator, sizeof(UniformBufferObject), 1);
|
||||
m_PreviewUbo->Update(0, &ubo, sizeof(ubo));
|
||||
|
||||
const glm::mat4 identity(1.0f);
|
||||
m_ObjectBuffer = CreateScope<UniformBuffer>(
|
||||
device, allocator, sizeof(glm::mat4), 1,
|
||||
vk::BufferUsageFlagBits::eStorageBuffer);
|
||||
m_ObjectBuffer->Update(0, &identity, sizeof(identity));
|
||||
m_PrevObjectBuffer = CreateScope<UniformBuffer>(
|
||||
device, allocator, sizeof(glm::mat4), 1,
|
||||
vk::BufferUsageFlagBits::eStorageBuffer);
|
||||
m_PrevObjectBuffer->Update(0, &identity, sizeof(identity));
|
||||
const u32 skinBase = 0; // 指向 palette slot0(identity)
|
||||
m_SkinOffsetBuffer = CreateScope<UniformBuffer>(
|
||||
device, allocator, sizeof(u32), 1,
|
||||
vk::BufferUsageFlagBits::eStorageBuffer);
|
||||
m_SkinOffsetBuffer->Update(0, &skinBase, sizeof(skinBase));
|
||||
m_SkinPaletteBuffer = CreateScope<UniformBuffer>(
|
||||
device, allocator, sizeof(glm::mat4), 1,
|
||||
vk::BufferUsageFlagBits::eStorageBuffer);
|
||||
m_SkinPaletteBuffer->Update(0, &identity, sizeof(identity));
|
||||
const u32 entityId[4] = {0, 0, 0, 0};
|
||||
m_EntityIdBuffer = CreateScope<UniformBuffer>(
|
||||
device, allocator, sizeof(entityId), 1,
|
||||
vk::BufferUsageFlagBits::eStorageBuffer);
|
||||
m_EntityIdBuffer->Update(0, entityId, sizeof(entityId));
|
||||
|
||||
m_Set0 = descriptors.AllocateDescriptorSet(gpu.uboSetLayout);
|
||||
descriptors.UpdateUniformBuffer(m_Set0, 0, m_PreviewUbo->Get(0),
|
||||
m_PreviewUbo->GetSize());
|
||||
descriptors.UpdateStorageBuffer(m_Set0, 1, m_ObjectBuffer->Get(0),
|
||||
m_ObjectBuffer->GetSize());
|
||||
descriptors.UpdateStorageBuffer(m_Set0, 2, m_PrevObjectBuffer->Get(0),
|
||||
m_PrevObjectBuffer->GetSize());
|
||||
descriptors.UpdateStorageBuffer(m_Set0, 3, m_SkinOffsetBuffer->Get(0),
|
||||
m_SkinOffsetBuffer->GetSize());
|
||||
descriptors.UpdateStorageBuffer(m_Set0, 4, m_SkinPaletteBuffer->Get(0),
|
||||
m_SkinPaletteBuffer->GetSize());
|
||||
descriptors.UpdateStorageBuffer(m_Set0, 5, m_EntityIdBuffer->Get(0),
|
||||
m_EntityIdBuffer->GetSize());
|
||||
|
||||
// ---- 球 mesh + tonemap per-slot set ----
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<u32> indices;
|
||||
BuildSphere(vertices, indices);
|
||||
m_Sphere = CreateScope<Mesh>(device, allocator, vertices,
|
||||
std::vector<std::vector<u32>>{indices},
|
||||
/*material*/ nullptr);
|
||||
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_TonemapSets[slot] = fullscreen.AllocateSet();
|
||||
}
|
||||
}
|
||||
|
||||
void MaterialPreviewPass::Register(RG::RenderGraph& rg)
|
||||
{
|
||||
// ---- 球体渲染:HDR atlas(loadOp=Load 保留缓存内容,逐脏 tile
|
||||
// 清矩形 + 视口绘制;表空帧 = 零 GPU 工作)----
|
||||
rg.AddPass("MaterialPreview", [this](RG::RenderGraphContext& ctx) {
|
||||
const vk::CommandBuffer cmd = ctx.cmd;
|
||||
|
||||
vk::ClearValue clearValue;
|
||||
// 背景色(仅用于逐 tile clearAttachments;tile 内容跨帧持久,
|
||||
// 只有脏 tile 被"清矩形 + 重画")
|
||||
clearValue.color = vk::ClearColorValue(
|
||||
std::array<float, 4>{0.06f, 0.06f, 0.07f, 1.0f});
|
||||
// velocity 附件:forward.frag 双输出接口契约(location 1),
|
||||
// 内容无人读——DontCare 免掉全图清屏(缓存命中帧零全图操作)
|
||||
vk::ClearValue velocityClear;
|
||||
velocityClear.color = vk::ClearColorValue(
|
||||
std::array<float, 4>{0.0f, 0.0f, 0.0f, 0.0f});
|
||||
|
||||
std::array<vk::RenderingAttachmentInfo, 2> attachments;
|
||||
attachments[0].setImageView(ctx.View(m_HDRAtlas))
|
||||
.setImageLayout(
|
||||
vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(clearValue);
|
||||
attachments[1].setImageView(ctx.View(m_VelocityAtlas))
|
||||
.setImageLayout(
|
||||
vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eDontCare)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eDontCare)
|
||||
.setClearValue(velocityClear);
|
||||
|
||||
vk::RenderingInfo renderingInfo;
|
||||
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
|
||||
.setLayerCount(1)
|
||||
.setColorAttachments(attachments);
|
||||
|
||||
cmd.beginRendering(renderingInfo);
|
||||
|
||||
// ---- 公共渲染状态(shader object 会残留上一 pass 的绑定,
|
||||
// 必须无条件直写——放在请求条件内时自检/空帧会复用
|
||||
// Outline 等前一 pass 的 shader/矩阵画出 garbage)----
|
||||
// 与场景同一 shader 对(WYSIWYG 关键:PBR 数学零复制)
|
||||
std::array<vk::ShaderStageFlagBits, 2> stages = {
|
||||
vk::ShaderStageFlagBits::eVertex,
|
||||
vk::ShaderStageFlagBits::eFragment};
|
||||
std::array<vk::ShaderEXT, 2> shaders = {
|
||||
m_Gpu.forwardVert.GetHandle(),
|
||||
m_Gpu.forwardFrag.GetHandle()};
|
||||
cmd.bindShadersEXT(stages, shaders);
|
||||
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 0, m_Set0, {});
|
||||
// 锁定环境集:IBL 三件套固定中性环境(观察条件恒定),
|
||||
// 级联/场景色副本与场景同步(previewEnvSets)
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 2,
|
||||
m_Gpu.previewEnvSets[ctx.frame.frameSlot],
|
||||
{});
|
||||
|
||||
cmd.setRasterizerDiscardEnable(VK_FALSE);
|
||||
// 无深度附件:凸球 + 背面剔除即无遮挡序
|
||||
cmd.setDepthTestEnable(VK_FALSE);
|
||||
cmd.setDepthWriteEnable(VK_FALSE);
|
||||
cmd.setDepthBoundsTestEnable(VK_FALSE);
|
||||
cmd.setStencilTestEnable(VK_FALSE);
|
||||
cmd.setDepthBiasEnable(VK_FALSE);
|
||||
cmd.setPolygonModeEXT(vk::PolygonMode::eFill);
|
||||
// 场景双面(cull none)对凸球会里外翻叠——预览球是封闭
|
||||
// 几何,剔背面才是正确遮挡语义
|
||||
cmd.setCullMode(vk::CullModeFlagBits::eBack);
|
||||
cmd.setFrontFace(vk::FrontFace::eCounterClockwise);
|
||||
cmd.setPrimitiveTopology(
|
||||
vk::PrimitiveTopology::eTriangleList);
|
||||
cmd.setPrimitiveRestartEnable(VK_FALSE);
|
||||
cmd.setVertexInputEXT(Vertex::GetBindingDescription(),
|
||||
Vertex::GetAttributeDescriptions());
|
||||
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
|
||||
vk::SampleMask sampleMask = 0xFFFFFFFF;
|
||||
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
|
||||
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
|
||||
cmd.setAlphaToOneEnableEXT(VK_FALSE);
|
||||
|
||||
if (!m_DirtyTiles.empty()) {
|
||||
// 逐脏 tile:清矩形(擦掉旧材质残影)→ 画球。
|
||||
// 同 tile 重复入队:池内已去重,此处防御性再滤
|
||||
u32 tileMask = 0;
|
||||
for (const auto& dirty : m_DirtyTiles) {
|
||||
if (!dirty.material ||
|
||||
dirty.tile >= kPreviewTileCount ||
|
||||
(tileMask & (1u << dirty.tile))) {
|
||||
continue;
|
||||
}
|
||||
tileMask |= 1u << dirty.tile;
|
||||
|
||||
const u32 col = dirty.tile % kPreviewTilesPerSide;
|
||||
const u32 row = dirty.tile / kPreviewTilesPerSide;
|
||||
vk::ClearAttachment tileClear;
|
||||
tileClear.setAspectMask(
|
||||
vk::ImageAspectFlagBits::eColor)
|
||||
.setColorAttachment(0)
|
||||
.setClearValue(clearValue);
|
||||
vk::ClearRect tileRect(
|
||||
vk::Rect2D(
|
||||
vk::Offset2D{i32(col * kPreviewTileSize),
|
||||
i32(row * kPreviewTileSize)},
|
||||
vk::Extent2D{kPreviewTileSize,
|
||||
kPreviewTileSize}),
|
||||
0, 1);
|
||||
cmd.clearAttachments(tileClear, tileRect);
|
||||
|
||||
RecordBall(cmd, dirty.tile, *dirty.material);
|
||||
}
|
||||
}
|
||||
|
||||
cmd.endRendering();
|
||||
}).Write(m_HDRAtlas, RG::ResourceUse::ColorAttachment)
|
||||
.Write(m_VelocityAtlas, RG::ResourceUse::ColorAttachment);
|
||||
|
||||
// ---- tonemap:HDR atlas → SDR atlas(ImGui 采样目标)----
|
||||
// 固定曝光 + ACES:材质球观察条件恒定,不随场景曝光设置漂移。
|
||||
// 仅有渲染的帧执行(缓存命中帧 SDR 内容不变,跳过 = 零 GPU 工作)
|
||||
rg.AddPass("MaterialPreviewTonemap",
|
||||
[this](RG::RenderGraphContext& ctx) {
|
||||
if (m_DirtyTiles.empty()) return;
|
||||
m_Fullscreen.RecordFullscreen(
|
||||
ctx, ctx.View(m_SDRAtlas), m_Fullscreen.tonemapFrag,
|
||||
m_TonemapSets[ctx.frame.frameSlot],
|
||||
FullscreenPush{1.0f, 1.0f, 0.0f, 0.0f});
|
||||
})
|
||||
.Read(m_HDRAtlas, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
.Write(m_SDRAtlas, RG::ResourceUse::ColorAttachment);
|
||||
}
|
||||
|
||||
void MaterialPreviewPass::RecordBall(vk::CommandBuffer cmd, u32 tile,
|
||||
const Material& material) const
|
||||
{
|
||||
const u32 col = tile % kPreviewTilesPerSide;
|
||||
const u32 row = tile / kPreviewTilesPerSide;
|
||||
const f32 x = f32(col * kPreviewTileSize);
|
||||
const f32 y = f32(row * kPreviewTileSize);
|
||||
|
||||
cmd.setViewportWithCount(vk::Viewport(
|
||||
x, y, f32(kPreviewTileSize), f32(kPreviewTileSize), 0.0f, 1.0f));
|
||||
cmd.setScissorWithCount(vk::Rect2D(
|
||||
vk::Offset2D{i32(col * kPreviewTileSize),
|
||||
i32(row * kPreviewTileSize)},
|
||||
vk::Extent2D{kPreviewTileSize, kPreviewTileSize}));
|
||||
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 1,
|
||||
material.GetDescriptorSet(), {});
|
||||
|
||||
// 混合态显式直写(shader object 动态状态跨 pass/请求残留)
|
||||
cmd.setColorBlendEnableEXT(
|
||||
0, material.IsTransparent() ? VK_TRUE : VK_FALSE);
|
||||
cmd.setColorBlendEquationEXT(
|
||||
0, material.IsTransparent() ? MakePremulEquation()
|
||||
: MakeDirectEquation());
|
||||
cmd.setColorWriteMaskEXT(0, kColorWriteMask);
|
||||
|
||||
m_Sphere->Draw(cmd, 1, 0, 0);
|
||||
}
|
||||
|
||||
void MaterialPreviewPass::OnTargetsRebuilt(RG::RenderGraph& rg)
|
||||
{
|
||||
// 终值路径(binding 0/1)= HDR atlas 物理镜像(重编译后重绑);
|
||||
// binding1 同源占位——tonemap.frag 终值路径恒采样 bloom 图,
|
||||
// 强度 0 丢弃。
|
||||
// binding 2-7 = 2D 调试视图槽:预览恒 debugView=0 不采样,但
|
||||
// 布局声明即需有效描述符(validation VUID-08114)——HDR atlas
|
||||
// 占位(类型匹配 sampler2D)。binding 8-11(cube 三件套 + LUT)
|
||||
// 走 OnEnvironmentChanged 绑真环境资源(cube 槽类型必须 Cube)
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_Fullscreen.UpdateSetInput(m_TonemapSets[slot], 0, rg,
|
||||
m_HDRAtlas, slot);
|
||||
m_Fullscreen.UpdateSetInput(m_TonemapSets[slot], 1, rg,
|
||||
m_HDRAtlas, slot);
|
||||
for (u32 b = 2; b < 8; ++b) {
|
||||
m_Fullscreen.UpdateSetInput(m_TonemapSets[slot], b, rg,
|
||||
m_HDRAtlas, slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MaterialPreviewPass::OnEnvironmentChanged(const Cubemap* env)
|
||||
{
|
||||
m_Env = env;
|
||||
if (!m_Env) return;
|
||||
|
||||
// 与 TonemapPass 相同的调试视图源(binding 8-11):cube 三件套
|
||||
// + LUT。Renderer 注册时与 SetEnvironment 时各通知一次,静态
|
||||
// 绑定超集模式——只在切换环境时重绑,每帧零成本
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_Descriptors.UpdateTexture(m_TonemapSets[slot], 8,
|
||||
m_Env->GetSampler(),
|
||||
m_Env->GetView());
|
||||
m_Descriptors.UpdateTexture(m_TonemapSets[slot], 9,
|
||||
m_Env->GetSampler(),
|
||||
m_Env->GetIrradianceView());
|
||||
m_Descriptors.UpdateTexture(m_TonemapSets[slot], 10,
|
||||
m_Env->GetSampler(),
|
||||
m_Env->GetPrefilteredView());
|
||||
m_Descriptors.UpdateTexture(m_TonemapSets[slot], 11,
|
||||
m_Env->GetBrdfLutSampler(),
|
||||
m_Env->GetBrdfLutView());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Engine::Passes
|
||||
@@ -61,14 +61,15 @@ namespace Engine::Passes
|
||||
textureSetLayout = descriptors.CreateLayout(texBindings);
|
||||
|
||||
// Set 2: CSM 级联阴影图(0/1/2)+ IBL 环境三件套(3 irradiance
|
||||
// / 4 pre-filtered / 5 BRDF LUT),均 fragment 采样
|
||||
// / 4 pre-filtered / 5 BRDF LUT)+ 场景色副本(6,透射折射采样,
|
||||
// 仅 translucent frag 声明),均 fragment 采样
|
||||
std::vector<DescriptorBinding> shadowBindings;
|
||||
for (u32 c = 0; c < kMaxCascades; ++c) {
|
||||
shadowBindings.push_back(
|
||||
{c, vk::DescriptorType::eCombinedImageSampler,
|
||||
vk::ShaderStageFlagBits::eFragment});
|
||||
}
|
||||
for (const u32 b : {3u, 4u, 5u}) {
|
||||
for (const u32 b : {3u, 4u, 5u, 6u}) {
|
||||
shadowBindings.push_back(
|
||||
{b, vk::DescriptorType::eCombinedImageSampler,
|
||||
vk::ShaderStageFlagBits::eFragment});
|
||||
@@ -141,9 +142,24 @@ namespace Engine::Passes
|
||||
.setCompareOp(vk::CompareOp::eLess);
|
||||
shadowSampler = device.GetDevice().createSampler(shadowSamplerInfo);
|
||||
|
||||
// 场景色副本采样器:普通线性 clamp-to-edge(折射采样用)
|
||||
vk::SamplerCreateInfo sceneSamplerInfo;
|
||||
sceneSamplerInfo.setMagFilter(vk::Filter::eLinear)
|
||||
.setMinFilter(vk::Filter::eLinear)
|
||||
.setMipmapMode(vk::SamplerMipmapMode::eNearest)
|
||||
.setAddressModeU(vk::SamplerAddressMode::eClampToEdge)
|
||||
.setAddressModeV(vk::SamplerAddressMode::eClampToEdge)
|
||||
.setAddressModeW(vk::SamplerAddressMode::eClampToEdge)
|
||||
.setUnnormalizedCoordinates(VK_FALSE);
|
||||
sceneSampler = device.GetDevice().createSampler(sceneSamplerInfo);
|
||||
|
||||
// per-slot 描述符分配 + Set0 绑定(buffer 句柄全程稳定,不用重绑)
|
||||
uboSets.resize(kMaxFramesInFlight);
|
||||
shadowSets.resize(kMaxFramesInFlight);
|
||||
#ifdef HELLO_EDITOR
|
||||
// 材质预览锁定环境集(editor 专属,game 构建不分配)
|
||||
previewEnvSets.resize(kMaxFramesInFlight);
|
||||
#endif
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
uboSets[slot] = descriptors.AllocateDescriptorSet(uboSetLayout);
|
||||
descriptors.UpdateUniformBuffer(
|
||||
@@ -164,6 +180,10 @@ namespace Engine::Passes
|
||||
entityIdBuffer.GetSize());
|
||||
|
||||
shadowSets[slot] = descriptors.AllocateDescriptorSet(shadowSetLayout);
|
||||
#ifdef HELLO_EDITOR
|
||||
previewEnvSets[slot] =
|
||||
descriptors.AllocateDescriptorSet(shadowSetLayout);
|
||||
#endif
|
||||
}
|
||||
// shadowSets 的图片绑定在 Compile 后由 BindShadowMap 完成
|
||||
|
||||
@@ -192,6 +212,7 @@ namespace Engine::Passes
|
||||
|
||||
// shader 随 ShaderRef 引用计数自动销毁,此处只清其余原生句柄
|
||||
vkDevice.destroySampler(shadowSampler);
|
||||
vkDevice.destroySampler(sceneSampler);
|
||||
vkDevice.destroyPipelineLayout(forwardLayout);
|
||||
vkDevice.destroyPipelineLayout(shadowLayout);
|
||||
vkDevice.destroyDescriptorSetLayout(uboSetLayout);
|
||||
@@ -203,15 +224,37 @@ namespace Engine::Passes
|
||||
const std::array<RG::TextureHandle, kMaxCascades>& maps)
|
||||
{
|
||||
// 物理镜像按帧槽位各一份:每个槽位的描述符绑各自的 view
|
||||
//(previewEnvSets 同步重绑——级联是几何资源,预览共用)
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
for (u32 c = 0; c < kMaxCascades; ++c) {
|
||||
m_Descriptors.UpdateTexture(
|
||||
shadowSets[slot], c, shadowSampler,
|
||||
rg.GetPhysicalView(maps[c], slot));
|
||||
#ifdef HELLO_EDITOR
|
||||
m_Descriptors.UpdateTexture(
|
||||
previewEnvSets[slot], c, shadowSampler,
|
||||
rg.GetPhysicalView(maps[c], slot));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SceneGpu::BindSceneColor(RG::RenderGraph& rg,
|
||||
RG::TextureHandle sceneColor)
|
||||
{
|
||||
// 物理镜像按帧槽位各一份,随 RG 重编译重绑(同 BindShadowMaps)
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_Descriptors.UpdateTexture(
|
||||
shadowSets[slot], 6, sceneSampler,
|
||||
rg.GetPhysicalView(sceneColor, slot));
|
||||
#ifdef HELLO_EDITOR
|
||||
m_Descriptors.UpdateTexture(
|
||||
previewEnvSets[slot], 6, sceneSampler,
|
||||
rg.GetPhysicalView(sceneColor, slot));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// ---- MDI:per-slot 上传 + 组绘制 ----
|
||||
|
||||
void SceneGpu::UploadIndirect(u32 frameSlot, const FrameSceneData& frame)
|
||||
@@ -275,4 +318,25 @@ namespace Engine::Passes
|
||||
}
|
||||
}
|
||||
|
||||
void SceneGpu::LockPreviewEnvironment(const Cubemap& env)
|
||||
{
|
||||
#ifdef HELLO_EDITOR
|
||||
// 只绑 previewEnvSets 的 IBL 三件套(binding 3/4/5),此后任何
|
||||
// SetEnvironment 都不影响预览——材质球观察条件恒定
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_Descriptors.UpdateTexture(previewEnvSets[slot], 3,
|
||||
env.GetSampler(),
|
||||
env.GetIrradianceView());
|
||||
m_Descriptors.UpdateTexture(previewEnvSets[slot], 4,
|
||||
env.GetSampler(),
|
||||
env.GetPrefilteredView());
|
||||
m_Descriptors.UpdateTexture(previewEnvSets[slot], 5,
|
||||
env.GetBrdfLutSampler(),
|
||||
env.GetBrdfLutView());
|
||||
}
|
||||
#else
|
||||
(void)env;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace Engine::Passes
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
#include <Engine/Renderer/Passes/PhysicsDebugPass.h>
|
||||
#include <Engine/Renderer/Passes/HitProxyPass.h>
|
||||
#include <Engine/Renderer/Passes/SelectionMaskPass.h>
|
||||
#ifdef HELLO_EDITOR
|
||||
#include <Engine/Renderer/Passes/EditorGizmoPass.h>
|
||||
#endif
|
||||
#include <Engine/Renderer/Passes/OutlinePass.h>
|
||||
#include <Engine/Physics/PhysicsDebugTypes.h>
|
||||
#include <Engine/Renderer/Cubemap.h>
|
||||
@@ -111,6 +114,23 @@ namespace Engine
|
||||
u64(Passes::PhysicsDebugPass::kMaxTriVerts) *
|
||||
sizeof(PhysicsDebugVertex),
|
||||
kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer);
|
||||
#ifdef HELLO_EDITOR
|
||||
// 3.3 Editor gizmo 图元 ring VB(editor 专属;game 构建不分配)。
|
||||
// eVertexBuffer 复用 UniformBuffer 的 per-slot fence 语义,
|
||||
// Render 上传窗口直灌(pass 无 PrepareFrame)
|
||||
m_GizmoIconVB = CreateScope<UniformBuffer>(
|
||||
*m_Device, *m_Allocator,
|
||||
u64(kMaxGizmoIconVerts) * sizeof(GizmoQuadVertex),
|
||||
kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer);
|
||||
m_GizmoWireVB = CreateScope<UniformBuffer>(
|
||||
*m_Device, *m_Allocator,
|
||||
u64(kMaxGizmoWireVerts) * sizeof(GizmoWireVertex),
|
||||
kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer);
|
||||
m_GizmoProxyVB = CreateScope<UniformBuffer>(
|
||||
*m_Device, *m_Allocator,
|
||||
u64(kMaxGizmoProxyVerts) * sizeof(GizmoQuadVertex),
|
||||
kMaxFramesInFlight, vk::BufferUsageFlagBits::eVertexBuffer);
|
||||
#endif
|
||||
|
||||
// 3.5 环境预积分生成端(compute 链)+ 中性兜底四件套。
|
||||
// 真实环境由 Application 经 AssetManager 加载后 SetEnvironment
|
||||
@@ -146,6 +166,10 @@ namespace Engine
|
||||
}
|
||||
// 环境辐照度绑定(view 在单个环境内全程稳定,绑一次)
|
||||
m_SceneGpu->BindEnvironment(*m_Environment);
|
||||
// 材质预览锁定环境:此时 m_Environment 必为中性兜底(L119 刚建,
|
||||
// SetEnvironment 只会发生在场景/资产加载之后)——材质球观察条件
|
||||
// 从此恒定,不随场景环境切换漂移(UE ThumbnailRenderer 语义)
|
||||
m_SceneGpu->LockPreviewEnvironment(*m_Environment);
|
||||
// 环境消费端 pass(Skybox 背景 / Tonemap 调试视图)同步初始绑定
|
||||
for (auto& pass : m_Passes) {
|
||||
pass->OnEnvironmentChanged(m_Environment.get());
|
||||
@@ -224,6 +248,34 @@ namespace Engine
|
||||
}
|
||||
void Renderer::SetBloomEnabled(bool b) { m_SceneRenderer->SetBloomEnabled(b); }
|
||||
bool Renderer::GetLodEnabled() const { return m_SceneRenderer->Frame().lodEnabled; }
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
// ---- 骨骼可视化:转发 SceneRenderer(编辑形态专属)----
|
||||
bool Renderer::GetBoneWiresVisible() const
|
||||
{
|
||||
return m_SceneRenderer->GetBoneWiresVisible();
|
||||
}
|
||||
void Renderer::SetBoneWiresVisible(bool b)
|
||||
{
|
||||
m_SceneRenderer->SetBoneWiresVisible(b);
|
||||
}
|
||||
bool Renderer::GetBoneAllNodes() const
|
||||
{
|
||||
return m_SceneRenderer->GetBoneAllNodes();
|
||||
}
|
||||
void Renderer::SetBoneAllNodes(bool b)
|
||||
{
|
||||
m_SceneRenderer->SetBoneAllNodes(b);
|
||||
}
|
||||
f32 Renderer::GetBoneJointPx() const
|
||||
{
|
||||
return m_SceneRenderer->GetBoneJointPx();
|
||||
}
|
||||
void Renderer::SetBoneJointPx(f32 px)
|
||||
{
|
||||
m_SceneRenderer->SetBoneJointPx(px);
|
||||
}
|
||||
#endif
|
||||
void Renderer::SetLodEnabled(bool b) { m_SceneRenderer->SetLodEnabled(b); }
|
||||
int Renderer::GetDebugView() const { return m_SceneRenderer->Frame().debugView; }
|
||||
void Renderer::SetDebugView(int v) { m_SceneRenderer->SetDebugView(v); }
|
||||
@@ -368,6 +420,13 @@ namespace Engine
|
||||
"HDRColor",
|
||||
RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat});
|
||||
|
||||
// 场景色副本:不透明段快照(含天空盒背景),透射折射采样源。
|
||||
// 每帧一次 copyImage(Forward 后、ForwardTrans 前),独立于
|
||||
// HDRColor 生命周期(后者还会被透明段/TAA 继续写入)
|
||||
m_SceneColor = m_RenderGraph->CreateTexture(
|
||||
"SceneColor",
|
||||
RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat});
|
||||
|
||||
// velocity:clip 空间运动向量(TAA 反推历史像素位置)
|
||||
m_Velocity = m_RenderGraph->CreateTexture(
|
||||
"Velocity",
|
||||
@@ -404,6 +463,29 @@ namespace Engine
|
||||
"TemporalDiff",
|
||||
RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat, 0, 0, 0.5f});
|
||||
|
||||
// 材质球预览 atlas:固定 1024²(不随渲染分辨率缩放——预览球
|
||||
// 256²/tile 观察条件恒定,编辑参数时对比才有意义)。
|
||||
// editor 专属:game 构建不创建(tile 内容跨帧持久,缓存语义)
|
||||
#ifdef HELLO_EDITOR
|
||||
m_PreviewHDR = m_RenderGraph->CreateTexture(
|
||||
"PreviewHDR",
|
||||
RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat,
|
||||
Passes::kPreviewAtlasSize,
|
||||
Passes::kPreviewAtlasSize});
|
||||
m_PreviewSDR = m_RenderGraph->CreateTexture(
|
||||
"PreviewSDR",
|
||||
RG::TextureDesc{vk::Format::eR8G8B8A8Unorm,
|
||||
Passes::kPreviewAtlasSize,
|
||||
Passes::kPreviewAtlasSize});
|
||||
// velocity 附件:forward.frag 双输出接口契约(预览内容恒 0,
|
||||
// 无读者——shader 零变体的代价是 1024² RGBA16F 显存)
|
||||
m_PreviewVelocity = m_RenderGraph->CreateTexture(
|
||||
"PreviewVelocity",
|
||||
RG::TextureDesc{vk::Format::eR16G16B16A16Sfloat,
|
||||
Passes::kPreviewAtlasSize,
|
||||
Passes::kPreviewAtlasSize});
|
||||
#endif
|
||||
|
||||
// ---- pass 注册:执行序由拓扑排序决定,与注册顺序无关 ----
|
||||
// 要加新 pass:new 一个 RenderPass 子类,在这里 push 一行。
|
||||
FrameSceneData& frame = m_SceneRenderer->Frame();
|
||||
@@ -414,7 +496,8 @@ namespace Engine
|
||||
*m_Device, *m_ShaderManager, *m_DescriptorManager, frame,
|
||||
m_HDRColor, m_Environment.get()));
|
||||
m_Passes.push_back(CreateScope<Passes::ForwardPass>(
|
||||
*m_SceneGpu, frame, m_HDRColor, m_Velocity, m_Depth, m_ShadowMaps));
|
||||
*m_SceneGpu, frame, m_HDRColor, m_Velocity, m_Depth,
|
||||
m_SceneColor, m_ShadowMaps));
|
||||
// 物理调试叠加:写 HDRColor(WAW 链排 Forward 之后,RAW 链在
|
||||
// TAA 之前)——调试图元进 TAA/Bloom/Tonemap 主链路
|
||||
m_Passes.push_back(CreateScope<Passes::PhysicsDebugPass>(
|
||||
@@ -422,6 +505,14 @@ namespace Engine
|
||||
frame, m_HDRColor, m_Depth, *m_PhysicsLineVB, *m_PhysicsTriVB));
|
||||
m_PhysicsDebugPass =
|
||||
static_cast<Passes::PhysicsDebugPass*>(m_Passes.back().get());
|
||||
#ifdef HELLO_EDITOR
|
||||
// Editor gizmo(billboard 图标 + 视锥线框):editor 专属 pass,
|
||||
// game 构建不注册不分配。Write(Depth) 的深度 WAW 边把自己固定
|
||||
// 排在 HitProxy 清深度之前(gizmo 深度测试消费场景深度)
|
||||
m_Passes.push_back(CreateScope<Passes::EditorGizmoPass>(
|
||||
*m_Device, *m_ShaderManager, *m_DescriptorManager,
|
||||
frame, m_HDRColor, m_Depth, *m_GizmoIconVB, *m_GizmoWireVB));
|
||||
#endif
|
||||
// UE 式 GPU 拾取:请求帧把 id 画进 R32Uint + 拷贝请求像素进
|
||||
// host 暂存(深度 WAW 链尾,排在 Forward/ForwardTrans/PhysicsDebug
|
||||
// 之后;App 层经 RequestPick/TakePickResult 消费)
|
||||
@@ -430,6 +521,10 @@ namespace Engine
|
||||
*m_SceneGpu, frame, m_HitProxyId, m_Depth));
|
||||
m_HitProxyPass =
|
||||
static_cast<Passes::HitProxyPass*>(m_Passes.back().get());
|
||||
#ifdef HELLO_EDITOR
|
||||
// gizmo 代理壳 ring VB 接入拾取 pass(请求帧与 mesh 同 pass 重画)
|
||||
m_HitProxyPass->AttachGizmoProxy(*m_GizmoProxyVB);
|
||||
#endif
|
||||
// 选中描边链:mask(只画选中实体,无深度测试=完整投影轮廓,
|
||||
// UE 透墙可见语义)→ Outline(叠加在 Tonemap 后的 PresentTarget
|
||||
// 上,恒定 LDR 描边色)
|
||||
@@ -453,13 +548,25 @@ namespace Engine
|
||||
m_Passes.push_back(CreateScope<Passes::OutlinePass>(
|
||||
*m_FullscreenGpu, *m_ShaderManager, frame,
|
||||
m_SelectionMask, m_PresentTarget));
|
||||
// 材质球预览(独立链:PreviewHDR → tonemap → PreviewSDR,与场景
|
||||
// 链无数据交叠;editor 专属,HELLO_EDITOR 裁剪。UI 段 Request 标
|
||||
// 脏,Render 段 TakeRenders 换入当帧脏表经引用传入)
|
||||
#ifdef HELLO_EDITOR
|
||||
m_Passes.push_back(CreateScope<Passes::MaterialPreviewPass>(
|
||||
*m_Device, *m_Allocator, *m_DescriptorManager, *m_SceneGpu,
|
||||
*m_FullscreenGpu, m_PreviewHDR, m_PreviewVelocity, m_PreviewSDR,
|
||||
m_PreviewRenders));
|
||||
m_PreviewPass =
|
||||
static_cast<Passes::MaterialPreviewPass*>(m_Passes.back().get());
|
||||
#endif
|
||||
// Direct 模式适配器:PresentTarget → backbuffer(Viewport 模式空转)。
|
||||
// 必须注册在 ImGui 之前:两者都写 backbuffer,WAW 边按声明序
|
||||
// 连接(前 writer → 后 writer),反序会让 blit 盖掉 UI
|
||||
m_Passes.push_back(CreateScope<Passes::BlitPass>(
|
||||
frame, m_PresentTarget, m_Backbuffer));
|
||||
m_Passes.push_back(CreateScope<Passes::ImGuiPass>(
|
||||
&m_ImGuiLayer, frame, m_Backbuffer, m_PresentTarget));
|
||||
&m_ImGuiLayer, frame, m_Backbuffer, m_PresentTarget,
|
||||
m_PreviewSDR));
|
||||
m_Passes.push_back(CreateScope<Passes::PresentPass>(m_Backbuffer));
|
||||
|
||||
for (auto& pass : m_Passes) {
|
||||
@@ -545,6 +652,32 @@ namespace Engine
|
||||
return tex;
|
||||
}
|
||||
|
||||
// ---- 材质球预览(editor 专属,HELLO_EDITOR 裁剪)----
|
||||
// UI 构建段逐可见行幂等请求 → 池标脏(收集本帧活指针);
|
||||
// Render 段 TakeRenders 取预算内的脏 tile → pass 清矩形重画
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
i32 Renderer::RequestMaterialPreview(Material* material)
|
||||
{
|
||||
return m_PreviewPool.Request(material);
|
||||
}
|
||||
|
||||
void Renderer::DropMaterialPreview(const Material* material)
|
||||
{
|
||||
m_PreviewPool.DropMaterial(material);
|
||||
}
|
||||
|
||||
void* Renderer::GetMaterialPreviewTexture() const
|
||||
{
|
||||
void* tex = nullptr;
|
||||
if (m_ImGuiLayer) {
|
||||
tex = (void*)(intptr_t)
|
||||
m_ImGuiLayer->GetPreviewTexture(m_CurrentFrame);
|
||||
}
|
||||
return tex;
|
||||
}
|
||||
#endif
|
||||
|
||||
// RG 物理镜像按帧槽位各一份:每槽位注册一个 ImGui 描述符。
|
||||
// 首次编译后(SetImGuiLayer)与每次重编译后(HandleResize)调用
|
||||
void Renderer::RefreshSceneTextures()
|
||||
@@ -553,7 +686,21 @@ namespace Engine
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_ImGuiLayer->UpdateSceneTexture(
|
||||
slot, m_RenderGraph->GetPhysicalView(m_PresentTarget, slot));
|
||||
// 材质球 atlas 同槽位注册(语义与场景纹理一致)
|
||||
#ifdef HELLO_EDITOR
|
||||
if (m_PreviewSDR.Valid()) {
|
||||
m_ImGuiLayer->UpdatePreviewTexture(
|
||||
slot, m_RenderGraph->GetPhysicalView(m_PreviewSDR, slot));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#ifdef HELLO_EDITOR
|
||||
// 重编译 = 物理镜像全部换新(三槽位 atlas 内容清零)——池的
|
||||
// paintedSlots 记录全部失效,可见行 keepalive 请求会重新补画
|
||||
if (m_PreviewSDR.Valid()) {
|
||||
m_PreviewPool.InvalidateAll();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Renderer::ReadGpuTimes(u32 frameSlot)
|
||||
@@ -596,6 +743,8 @@ namespace Engine
|
||||
{
|
||||
ZoneScoped; // CPU 侧:UBO/SSBO 上传 + 录制 + 提交(GPU 耗时见 RG timestamp)
|
||||
// resize 已在 BeginFrameSync(UI 构建前)处理,此处无需再查
|
||||
//(预览脏表换入在本函数末尾、Execute 之前——所有跳帧路径之后,
|
||||
// 保证"标记已画"的槽位镜像必然真的被写入)
|
||||
|
||||
const u32 frameSlot = m_CurrentFrame;
|
||||
const auto& vkDevice = m_Device->GetDevice();
|
||||
@@ -669,6 +818,43 @@ namespace Engine
|
||||
m_PhysicsDebugPass->PrepareFrame(frameSlot, *m_PhysicsDebug);
|
||||
}
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
// Editor gizmo 图元同窗口上传(fence 已等);空帧不写,pass 端
|
||||
// 顶点数 0 短路零开销。三流各落各槽位(代理壳给 HitProxy 消费)
|
||||
if (!frame.gizmoIconVerts.empty()) {
|
||||
m_GizmoIconVB->Update(frameSlot, frame.gizmoIconVerts.data(),
|
||||
frame.gizmoIconVerts.size() *
|
||||
sizeof(GizmoQuadVertex));
|
||||
}
|
||||
if (!frame.gizmoWireVerts.empty()) {
|
||||
m_GizmoWireVB->Update(frameSlot, frame.gizmoWireVerts.data(),
|
||||
frame.gizmoWireVerts.size() *
|
||||
sizeof(GizmoWireVertex));
|
||||
}
|
||||
// 骨骼线与 gizmo 线共享同一 LineList 顶点缓冲:骨骼段追加在
|
||||
// gizmo 段之后(偏移写入),超容量截断(单实体骨架远打不满,
|
||||
// 只是防御 gizmo 线占满时的 OOB 写)
|
||||
if (!frame.boneWireVerts.empty()) {
|
||||
const u64 wireUsed = frame.gizmoWireVerts.size();
|
||||
const u64 capVerts =
|
||||
m_GizmoWireVB->GetSize() / sizeof(GizmoWireVertex);
|
||||
const u64 room = capVerts > wireUsed ? capVerts - wireUsed : 0;
|
||||
const u64 boneAvail = std::min<u64>(
|
||||
frame.boneWireVerts.size(), room);
|
||||
if (boneAvail > 0) {
|
||||
m_GizmoWireVB->UpdateAt(
|
||||
frameSlot, wireUsed * sizeof(GizmoWireVertex),
|
||||
frame.boneWireVerts.data(),
|
||||
boneAvail * sizeof(GizmoWireVertex));
|
||||
}
|
||||
}
|
||||
if (!frame.gizmoProxyVerts.empty()) {
|
||||
m_GizmoProxyVB->Update(frameSlot, frame.gizmoProxyVerts.data(),
|
||||
frame.gizmoProxyVerts.size() *
|
||||
sizeof(GizmoQuadVertex));
|
||||
}
|
||||
#endif
|
||||
|
||||
// 3. acquire swapchain image
|
||||
u32 imageIndex = 0;
|
||||
try {
|
||||
@@ -699,6 +885,13 @@ namespace Engine
|
||||
m_Swapchain->GetImages()[imageIndex],
|
||||
m_Swapchain->GetImageViews()[imageIndex]);
|
||||
|
||||
// UI 构建段标脏的预览请求换入当帧脏表(pass 引用读取;此处已过
|
||||
// 全部跳帧路径,标记 paintedSlots 的槽位镜像本帧必然被写入)。
|
||||
// 预算外的脏 tile 保持未取状态,下帧可见行 keepalive 请求重新入队
|
||||
#ifdef HELLO_EDITOR
|
||||
m_PreviewRenders = m_PreviewPool.TakeRenders(frameSlot);
|
||||
#endif
|
||||
|
||||
m_RenderGraph->Execute(frameCtx);
|
||||
|
||||
m_CommandManager->EndFrame(frameSlot);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <Engine/Renderer/Frustum.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Renderer/Mesh.h>
|
||||
#include <Engine/Renderer/MaterialAsset.h>
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
@@ -18,6 +19,7 @@
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
|
||||
#include <tracy/Tracy.hpp>
|
||||
@@ -31,17 +33,37 @@ namespace Engine
|
||||
|
||||
namespace
|
||||
{
|
||||
// 相机裁剪范围(UE 编辑器视口语义:near = 10cm;far 拉远到
|
||||
// "无限"级——UE 的 far clip 默认就是不裁剪,深度精度由 near 主导,
|
||||
// far 取多大都不损近处精度)。阴影距离独立(UE DirectionalLight
|
||||
// 的 shadow distance 同构):级联分割只覆盖 kShadowDistance,
|
||||
// 更远无阴影,避免级联跟着 far 拉爆近处阴影分辨率
|
||||
constexpr f32 kCameraNear = 0.1f;
|
||||
constexpr f32 kCameraFar = 10000.0f;
|
||||
// 相机裁剪范围不再走常量:每实体相机自带 near/far(Camera.h
|
||||
// 默认值 0.1/10000,UE 语义:near 主导深度精度,far 拉远到
|
||||
// "无限"级)。阴影距离独立(UE DirectionalLight 的 shadow
|
||||
// distance 同构):级联分割只覆盖 kShadowDistance,更远无阴影,
|
||||
// 避免级联跟着 far 拉爆近处阴影分辨率
|
||||
constexpr f32 kShadowDistance = 100.0f;
|
||||
// 实用分割插值因子(0=均匀,1=对数;0.6 是业界常用值)
|
||||
constexpr f32 kCascadeLambda = 0.6f;
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
// ---- Editor gizmo 收集常量(CollectEditorGizmos 专用)----
|
||||
// 图标屏幕占比(相对视口高):恒定屏幕尺寸 = dist·tanHalf·frac
|
||||
constexpr f32 kIconScreenFrac = 0.04f;
|
||||
// 代理棱边屏宽(像素):HitProxy 可点选的 ribbon 宽度
|
||||
constexpr f32 kProxyWidthPx = 4.0f;
|
||||
// 视锥线框可视远平面截断:far clip(10000m)全尺寸线框横贯场景
|
||||
// 无信息量,可视化统一用固定短距离(渲染语义不变,纯显示)
|
||||
constexpr f32 kGizmoFrustumVisFar = 3.0f;
|
||||
// 视锥棱 ribbon 细分段数(恒定屏宽的折线逼近)
|
||||
constexpr u32 kRibbonSegsPerEdge = 12;
|
||||
// 点光选中时的衰减半径圆段数
|
||||
constexpr u32 kRadiusCircleSegs = 24;
|
||||
|
||||
// billboard 形状 id(gizmo_billboard.frag 的 SDF 分支约定)
|
||||
constexpr f32 kShapeDirLight = 0.0f;
|
||||
constexpr f32 kShapePointLight = 1.0f;
|
||||
constexpr f32 kShapeCamera = 2.0f;
|
||||
constexpr f32 kShapeSkybox = 3.0f;
|
||||
constexpr f32 kShapeSkyLight = 4.0f;
|
||||
#endif
|
||||
|
||||
// Halton 低差异序列(TAA 子像素抖动)
|
||||
f32 Halton(u32 index, u32 base)
|
||||
{
|
||||
@@ -53,134 +75,20 @@ namespace Engine
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---- 骨骼动画采样(BuildFrame 第 2.5 节专用)----
|
||||
|
||||
// 静止 local TRS 分解(列主序 SRT 矩阵;假设无负缩放/剪切)。
|
||||
// 被动画轨道触碰的节点用关键帧重建,未触碰的保持原矩阵——
|
||||
// FBX PreRotation 折在 localTransform 里,拆 TRS 重建会丢
|
||||
void DecomposeTRS(const glm::mat4& m, glm::vec3& pos, glm::quat& rot,
|
||||
glm::vec3& scl)
|
||||
{
|
||||
pos = glm::vec3(m[3]);
|
||||
scl = {glm::length(glm::vec3(m[0])),
|
||||
glm::length(glm::vec3(m[1])),
|
||||
glm::length(glm::vec3(m[2]))};
|
||||
const glm::mat3 r(glm::vec3(m[0]) / scl.x,
|
||||
glm::vec3(m[1]) / scl.y,
|
||||
glm::vec3(m[2]) / scl.z);
|
||||
rot = glm::quat_cast(r);
|
||||
}
|
||||
|
||||
// 关键帧采样:前后键插值(位置/缩放 lerp,旋转 slerp)。
|
||||
// key 数通常几十个,线性扫描对 cache 更友好
|
||||
glm::vec3 SampleKeys(const std::vector<PosKey>& keys, f32 t,
|
||||
const glm::vec3& rest)
|
||||
{
|
||||
if (keys.empty()) return rest;
|
||||
if (t <= keys.front().time) return keys.front().value;
|
||||
for (size_t i = 0; i + 1 < keys.size(); ++i) {
|
||||
if (t < keys[i + 1].time) {
|
||||
const f32 a = keys[i].time, b = keys[i + 1].time;
|
||||
return glm::mix(keys[i].value, keys[i + 1].value,
|
||||
b > a ? (t - a) / (b - a) : 0.0f);
|
||||
}
|
||||
}
|
||||
return keys.back().value;
|
||||
}
|
||||
|
||||
glm::vec3 SampleKeys(const std::vector<SclKey>& keys, f32 t,
|
||||
const glm::vec3& rest)
|
||||
{
|
||||
if (keys.empty()) return rest;
|
||||
if (t <= keys.front().time) return keys.front().value;
|
||||
for (size_t i = 0; i + 1 < keys.size(); ++i) {
|
||||
if (t < keys[i + 1].time) {
|
||||
const f32 a = keys[i].time, b = keys[i + 1].time;
|
||||
return glm::mix(keys[i].value, keys[i + 1].value,
|
||||
b > a ? (t - a) / (b - a) : 0.0f);
|
||||
}
|
||||
}
|
||||
return keys.back().value;
|
||||
}
|
||||
|
||||
glm::quat SampleKeys(const std::vector<RotKey>& keys, f32 t,
|
||||
const glm::quat& rest)
|
||||
{
|
||||
if (keys.empty()) return rest;
|
||||
if (t <= keys.front().time) return keys.front().value;
|
||||
for (size_t i = 0; i + 1 < keys.size(); ++i) {
|
||||
if (t < keys[i + 1].time) {
|
||||
const f32 a = keys[i].time, b = keys[i + 1].time;
|
||||
return glm::slerp(keys[i].value, keys[i + 1].value,
|
||||
b > a ? (t - a) / (b - a) : 0.0f);
|
||||
}
|
||||
}
|
||||
return keys.back().value;
|
||||
}
|
||||
|
||||
// 单 clip 在 time 的全节点局部 TRS(无轨道分量 = 静止分解值)。
|
||||
// touched = 被任意轨道触碰的节点标记(ComposeGlobals 决定重建或原样)
|
||||
void SampleLocalPose(const Model& model, const AnimationClip& clip,
|
||||
f32 time, std::vector<glm::vec3>& pos,
|
||||
std::vector<glm::quat>& rot,
|
||||
std::vector<glm::vec3>& scl,
|
||||
std::vector<u8>& touched)
|
||||
{
|
||||
const auto& nodes = model.GetNodes();
|
||||
pos.resize(nodes.size());
|
||||
rot.resize(nodes.size());
|
||||
scl.resize(nodes.size());
|
||||
touched.assign(nodes.size(), 0);
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
DecomposeTRS(nodes[i].localTransform, pos[i], rot[i], scl[i]);
|
||||
}
|
||||
if (clip.duration <= 0.0f) return;
|
||||
const f32 t = glm::mod(time, clip.duration);
|
||||
for (const auto& ch : clip.channels) {
|
||||
if (ch.nodeIdx >= nodes.size()) continue;
|
||||
touched[ch.nodeIdx] = 1;
|
||||
if (!ch.positions.empty())
|
||||
pos[ch.nodeIdx] = SampleKeys(ch.positions, t, pos[ch.nodeIdx]);
|
||||
if (!ch.rotations.empty())
|
||||
rot[ch.nodeIdx] = SampleKeys(ch.rotations, t, rot[ch.nodeIdx]);
|
||||
if (!ch.scales.empty())
|
||||
scl[ch.nodeIdx] = SampleKeys(ch.scales, t, scl[ch.nodeIdx]);
|
||||
}
|
||||
}
|
||||
|
||||
// 局部 TRS → 全节点 global 矩阵(层级累乘;父序 DFS 保证父先算)
|
||||
void ComposeGlobals(const Model& model, const std::vector<glm::vec3>& pos,
|
||||
const std::vector<glm::quat>& rot,
|
||||
const std::vector<glm::vec3>& scl,
|
||||
const std::vector<u8>& touched,
|
||||
std::vector<glm::mat4>& globals)
|
||||
{
|
||||
const auto& nodes = model.GetNodes();
|
||||
globals.resize(nodes.size());
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
const glm::mat4 local = touched[i]
|
||||
? glm::translate(glm::mat4(1.0f), pos[i]) *
|
||||
glm::mat4_cast(rot[i]) * glm::scale(glm::mat4(1.0f), scl[i])
|
||||
: nodes[i].localTransform;
|
||||
globals[i] = nodes[i].parent >= 0
|
||||
? globals[static_cast<u32>(nodes[i].parent)] * local
|
||||
: local;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SceneRenderer::ComputeCascades(const glm::vec3& shadowDir)
|
||||
void SceneRenderer::ComputeCascades(const glm::vec3& shadowDir,
|
||||
const f32 camNear, const f32 camFar)
|
||||
{
|
||||
constexpr u32 N = kMaxCascades;
|
||||
|
||||
// ---- 1. 实用分割:均匀与对数分割按 λ 插值(视图深度,正值)----
|
||||
//(分割覆盖 [near, kShadowDistance];kCameraFar 只管主投影裁剪)
|
||||
//(分割覆盖 [camNear, kShadowDistance];camFar 只管主投影裁剪)
|
||||
f32 splits[N];
|
||||
for (u32 i = 1; i <= N; ++i) {
|
||||
const f32 t = static_cast<f32>(i) / N;
|
||||
const f32 uniform = kCameraNear + (kShadowDistance - kCameraNear) * t;
|
||||
const f32 log = kCameraNear * glm::pow(kShadowDistance / kCameraNear, t);
|
||||
const f32 uniform = camNear + (kShadowDistance - camNear) * t;
|
||||
const f32 log = camNear * glm::pow(kShadowDistance / camNear, t);
|
||||
splits[i - 1] = glm::mix(uniform, log, kCascadeLambda);
|
||||
}
|
||||
for (u32 c = 0; c < N; ++c) {
|
||||
@@ -203,14 +111,14 @@ namespace Engine
|
||||
? glm::vec3(0.0f, 0.0f, 1.0f) : glm::vec3(0.0f, 1.0f, 0.0f);
|
||||
|
||||
for (u32 c = 0; c < N; ++c) {
|
||||
const f32 d0 = (c == 0) ? kCameraNear : splits[c - 1];
|
||||
const f32 d0 = (c == 0) ? camNear : splits[c - 1];
|
||||
const f32 d1 = splits[c];
|
||||
// 视图深度 → 视锥棱边参数:棱边上 z_view 线性(near/far 角点
|
||||
// 都在各自平面上),farC 反投影自主投影 far 面,故 t 相对
|
||||
// kCameraFar 归一;splits 只覆盖 [near, kShadowDistance],
|
||||
// camFar 归一;splits 只覆盖 [camNear, kShadowDistance],
|
||||
// t 必然远小于 1 → 自然截出阴影范围内的切片
|
||||
const f32 t0 = (d0 - kCameraNear) / (kCameraFar - kCameraNear);
|
||||
const f32 t1 = (d1 - kCameraNear) / (kCameraFar - kCameraNear);
|
||||
const f32 t0 = (d0 - camNear) / (camFar - camNear);
|
||||
const f32 t1 = (d1 - camNear) / (camFar - camNear);
|
||||
|
||||
glm::vec3 slice[8];
|
||||
for (u32 i = 0; i < 4; ++i) {
|
||||
@@ -258,6 +166,297 @@ namespace Engine
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HELLO_EDITOR
|
||||
// ---- Editor gizmo 收集(编辑形态专属,只读场景写 editor 段)----
|
||||
void SceneRenderer::CollectEditorGizmos(ECS::Scene& scene,
|
||||
const glm::vec3& camPos,
|
||||
const glm::vec3& camRight,
|
||||
const glm::vec3& camUp,
|
||||
f32 aspect, f32 viewHeightPx,
|
||||
f32 viewTanHalf)
|
||||
{
|
||||
auto& icons = m_Frame.gizmoIconVerts;
|
||||
auto& wires = m_Frame.gizmoWireVerts;
|
||||
auto& proxies = m_Frame.gizmoProxyVerts;
|
||||
icons.clear();
|
||||
wires.clear();
|
||||
proxies.clear();
|
||||
|
||||
constexpr f32 kTwoPi = 6.28318530718f;
|
||||
// viewTanHalf:编辑器视角相机的 tan(fov/2)(BuildFrame 传入),
|
||||
// 只服务 billboard 恒定屏占比与 proxy ribbon 恒定像素宽——与
|
||||
// 相机实体自己的 fov 无关
|
||||
const f32 tanHalf = viewTanHalf;
|
||||
const bool anySelected = m_Frame.selectedEntityValid;
|
||||
const u32 selBits = m_Frame.selectedEntityBits;
|
||||
auto IsSel = [&](u32 bits)
|
||||
{ return anySelected && bits == selBits; };
|
||||
|
||||
// 预算哨兵(一次性 WARN 复用截断标志语义)
|
||||
bool truncated = false;
|
||||
auto Fit = [](u32 cur, u32 add, u32 cap)
|
||||
{ return cur + add <= cap; };
|
||||
|
||||
// 图标 billboard:面向相机 quad(2 三角)+ 同角点进代理流(可点选)
|
||||
auto EmitIcon = [&](const glm::vec3& pos, f32 shape,
|
||||
const glm::vec4& color, u32 bits, f32 scaleMul)
|
||||
{
|
||||
if (!Fit(static_cast<u32>(icons.size()), 6, kMaxGizmoIconVerts) ||
|
||||
!Fit(static_cast<u32>(proxies.size()), 6,
|
||||
kMaxGizmoProxyVerts))
|
||||
{
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
const f32 dist =
|
||||
std::max(glm::length(pos - camPos), 0.1f);
|
||||
const f32 half = dist * tanHalf * kIconScreenFrac * scaleMul;
|
||||
const glm::vec3 r = camRight * half;
|
||||
const glm::vec3 u = camUp * half;
|
||||
const glm::vec3 p00 = pos - r - u, p10 = pos + r - u;
|
||||
const glm::vec3 p11 = pos + r + u, p01 = pos - r + u;
|
||||
const auto push = [&](std::vector<GizmoQuadVertex>& v,
|
||||
const glm::vec3& p, f32 lu, f32 lv)
|
||||
{
|
||||
v.push_back({p, color, glm::vec4(shape, lu, lv, 0.0f), bits});
|
||||
};
|
||||
// 三角表:v00 v10 v11 / v00 v11 v01(uv 对应角点)
|
||||
push(icons, p00, -1, -1); push(icons, p10, 1, -1);
|
||||
push(icons, p11, 1, 1);
|
||||
push(icons, p00, -1, -1); push(icons, p11, 1, 1);
|
||||
push(icons, p01, -1, 1);
|
||||
// 代理壳颜色占位(拾取 frag 只写 entityId)
|
||||
const glm::vec4 any{1.0f};
|
||||
const auto pushP = [&](const glm::vec3& p)
|
||||
{ proxies.push_back({p, any, glm::vec4(shape, 0, 0, 0), bits}); };
|
||||
pushP(p00); pushP(p10); pushP(p11);
|
||||
pushP(p00); pushP(p11); pushP(p01);
|
||||
};
|
||||
|
||||
// 线段(LineList 成对 push)
|
||||
auto EmitLine = [&](const glm::vec3& a, const glm::vec3& b,
|
||||
const glm::vec4& color)
|
||||
{
|
||||
if (!Fit(static_cast<u32>(wires.size()), 2, kMaxGizmoWireVerts))
|
||||
{
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
wires.push_back({a, color});
|
||||
wires.push_back({b, color});
|
||||
};
|
||||
|
||||
// 拾取代理 ribbon:棱边 ab 细分 N 段,每段以段中点为基准的面向
|
||||
// 相机 quad(半宽恒定 ~kProxyWidthPx 屏幕像素)。线段在 R32Uint
|
||||
// 光栅化里点不中,quad 才可拾取
|
||||
auto EmitRibbon = [&](const glm::vec3& a, const glm::vec3& b,
|
||||
u32 bits)
|
||||
{
|
||||
const glm::vec3 ab = b - a;
|
||||
const f32 len = glm::length(ab);
|
||||
if (len < 1e-4f) return;
|
||||
const glm::vec3 dir = ab / len;
|
||||
for (u32 s = 0; s < kRibbonSegsPerEdge; ++s)
|
||||
{
|
||||
const f32 t0 = static_cast<f32>(s) / kRibbonSegsPerEdge;
|
||||
const f32 t1 = static_cast<f32>(s + 1) / kRibbonSegsPerEdge;
|
||||
const glm::vec3 m =
|
||||
a + ab * (0.5f * (t0 + t1)); // 段中点
|
||||
const glm::vec3 toCam = camPos - m;
|
||||
const f32 dist = std::max(glm::length(toCam), 0.1f);
|
||||
glm::vec3 side = glm::cross(dir, toCam / dist);
|
||||
const f32 sideLen = glm::length(side);
|
||||
if (!(sideLen > 1e-6f) ||
|
||||
!(std::isfinite(side.x) && std::isfinite(side.y) &&
|
||||
std::isfinite(side.z)))
|
||||
{
|
||||
side = camUp; // 棱正对相机退化:用屏幕 up 兜底
|
||||
}
|
||||
else
|
||||
{
|
||||
side /= sideLen;
|
||||
}
|
||||
const f32 half =
|
||||
dist * tanHalf * (kProxyWidthPx / viewHeightPx);
|
||||
const glm::vec3 halfDir = dir * (len / kRibbonSegsPerEdge * 0.5f);
|
||||
const glm::vec3 halfSide = side * half;
|
||||
if (!Fit(static_cast<u32>(proxies.size()), 6,
|
||||
kMaxGizmoProxyVerts))
|
||||
{
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
const glm::vec4 any{1.0f};
|
||||
const glm::vec3 p0 = m - halfDir - halfSide;
|
||||
const glm::vec3 p1 = m + halfDir - halfSide;
|
||||
const glm::vec3 p2 = m + halfDir + halfSide;
|
||||
const glm::vec3 p3 = m - halfDir + halfSide;
|
||||
proxies.push_back({p0, any, glm::vec4(0, 0, 0, 0), bits});
|
||||
proxies.push_back({p1, any, glm::vec4(0, 0, 0, 0), bits});
|
||||
proxies.push_back({p2, any, glm::vec4(0, 0, 0, 0), bits});
|
||||
proxies.push_back({p0, any, glm::vec4(0, 0, 0, 0), bits});
|
||||
proxies.push_back({p2, any, glm::vec4(0, 0, 0, 0), bits});
|
||||
proxies.push_back({p3, any, glm::vec4(0, 0, 0, 0), bits});
|
||||
}
|
||||
};
|
||||
|
||||
// 相机实体视锥线框(near 真值 / 远平面可视截断)+ 棱 ribbon。
|
||||
// 几何取实体相机自己的 fov/near/orthoSize(Inspector 参数改动
|
||||
// 即时反映到线框),aspect 用编辑器视口比例(可视化近似)
|
||||
auto EmitFrustum = [&](const ECS::TransformComponent& t,
|
||||
const ECS::CameraComponent& cc,
|
||||
const glm::vec4& lineColor, u32 bits)
|
||||
{
|
||||
const glm::mat4 W = t.World();
|
||||
const auto Xform = [&](f32 x, f32 y, f32 z)
|
||||
{ return glm::vec3(W * glm::vec4(x, y, z, 1.0f)); };
|
||||
// 本地 -Z 为视线方向(Camera 单一事实源约定)
|
||||
const f32 camNear = cc.camera.GetNearZ();
|
||||
// 可视远平面不小于 near 的 1.5 倍:用户把 near 拖大时防止
|
||||
// 远矩形反而落在 near 之前(线框翻转)
|
||||
const f32 visFar =
|
||||
glm::max(kGizmoFrustumVisFar, camNear * 1.5f);
|
||||
f32 nH = 0.0f, vH = 0.0f;
|
||||
if (cc.camera.GetProjectionMode() ==
|
||||
Camera::ProjectionMode::Orthographic)
|
||||
{
|
||||
// 正交视锥 = 长方体:near/远矩形同宽高(无透视收缩)
|
||||
nH = cc.camera.GetOrthoSize();
|
||||
vH = nH;
|
||||
}
|
||||
else
|
||||
{
|
||||
const f32 tanHalfEnt =
|
||||
std::tan(glm::radians(cc.camera.GetFovDeg()) * 0.5f);
|
||||
nH = tanHalfEnt * camNear;
|
||||
vH = tanHalfEnt * visFar;
|
||||
}
|
||||
const f32 nW = nH * aspect;
|
||||
const f32 vW = vH * aspect;
|
||||
const glm::vec3 n0 = Xform(-nW, -nH, -camNear);
|
||||
const glm::vec3 n1 = Xform(nW, -nH, -camNear);
|
||||
const glm::vec3 n2 = Xform(nW, nH, -camNear);
|
||||
const glm::vec3 n3 = Xform(-nW, nH, -camNear);
|
||||
const glm::vec3 f0 = Xform(-vW, -vH, -visFar);
|
||||
const glm::vec3 f1 = Xform(vW, -vH, -visFar);
|
||||
const glm::vec3 f2 = Xform(vW, vH, -visFar);
|
||||
const glm::vec3 f3 = Xform(-vW, vH, -visFar);
|
||||
// near 矩形 + 可视远矩形 + 4 棱 + 上轴刻度
|
||||
EmitLine(n0, n1, lineColor); EmitLine(n1, n2, lineColor);
|
||||
EmitLine(n2, n3, lineColor); EmitLine(n3, n0, lineColor);
|
||||
EmitLine(f0, f1, lineColor); EmitLine(f1, f2, lineColor);
|
||||
EmitLine(f2, f3, lineColor); EmitLine(f3, f0, lineColor);
|
||||
EmitLine(n0, f0, lineColor); EmitLine(n1, f1, lineColor);
|
||||
EmitLine(n2, f2, lineColor); EmitLine(n3, f3, lineColor);
|
||||
EmitLine(Xform(0.0f, nH, -camNear),
|
||||
Xform(0.0f, nH + 0.3f, -camNear), lineColor);
|
||||
// 12 棱的拾取 ribbon(图标 quad 之外的可点选面积)
|
||||
EmitRibbon(n0, n1, bits); EmitRibbon(n1, n2, bits);
|
||||
EmitRibbon(n2, n3, bits); EmitRibbon(n3, n0, bits);
|
||||
EmitRibbon(f0, f1, bits); EmitRibbon(f1, f2, bits);
|
||||
EmitRibbon(f2, f3, bits); EmitRibbon(f3, f0, bits);
|
||||
EmitRibbon(n0, f0, bits); EmitRibbon(n1, f1, bits);
|
||||
EmitRibbon(n2, f2, bits); EmitRibbon(n3, f3, bits);
|
||||
};
|
||||
|
||||
// 灯光:方向光 = 太阳盘 + 照射方向箭头;点光 = 光晕盘
|
||||
//(选中追加衰减半径三正交圆,UE 语义——常显是视觉噪音)
|
||||
scene.Each<TransformComponent, LightComponent>(
|
||||
[&](Entity e, TransformComponent& t, LightComponent& light)
|
||||
{
|
||||
if (!light.active) return;
|
||||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||
const glm::vec3 pos = t.GetWorldPosition();
|
||||
const glm::vec3 rgb(light.color);
|
||||
const glm::vec4 color =
|
||||
IsSel(bits) ? kGizmoSelectedColor
|
||||
: glm::vec4(rgb * (0.6f + 0.4f * light.color.a),
|
||||
1.0f);
|
||||
if (light.type == LightComponent::Type::Point)
|
||||
{
|
||||
EmitIcon(pos, kShapePointLight, color, bits, 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
EmitIcon(pos, kShapeDirLight, color, bits, 1.1f);
|
||||
glm::vec3 dir = light.direction;
|
||||
if (glm::length(dir) > 1e-4f)
|
||||
{
|
||||
dir = glm::normalize(dir);
|
||||
const glm::vec4 lineColor =
|
||||
IsSel(bits) ? kGizmoSelectedColor
|
||||
: glm::vec4(rgb, 0.9f);
|
||||
EmitLine(pos, pos + dir * 0.6f, lineColor);
|
||||
}
|
||||
}
|
||||
if (IsSel(bits) && light.type == LightComponent::Type::Point)
|
||||
{
|
||||
const glm::vec4 ring = glm::vec4(rgb, 0.35f);
|
||||
const f32 R = light.radius;
|
||||
for (u32 s = 0; s < kRadiusCircleSegs; ++s)
|
||||
{
|
||||
const f32 a0 = kTwoPi * s / kRadiusCircleSegs;
|
||||
const f32 a1 = kTwoPi * (s + 1) /
|
||||
kRadiusCircleSegs;
|
||||
const glm::vec3 d0(std::cos(a0), std::sin(a0), 0.0f);
|
||||
const glm::vec3 d1(std::cos(a1), std::sin(a1), 0.0f);
|
||||
EmitLine(pos + d0 * R, pos + d1 * R, ring); // XY
|
||||
EmitLine(pos + glm::vec3(d0.x, 0, d0.y) * R,
|
||||
pos + glm::vec3(d1.x, 0, d1.y) * R, ring); // XZ
|
||||
EmitLine(pos + glm::vec3(0, d0.y, d0.x) * R,
|
||||
pos + glm::vec3(0, d1.y, d1.x) * R, ring); // YZ
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 相机实体:图标 + 视锥线框(primary 提亮放大)
|
||||
scene.Each<TransformComponent, CameraComponent>(
|
||||
[&](Entity e, TransformComponent& t, CameraComponent& cam)
|
||||
{
|
||||
if (!cam.active) return;
|
||||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||
const bool sel = IsSel(bits);
|
||||
const glm::vec4 color =
|
||||
sel ? kGizmoSelectedColor
|
||||
: glm::vec4(cam.primary ? 1.0f : 0.55f, 0.8f, 1.0f, 1.0f);
|
||||
EmitIcon(t.GetWorldPosition(), kShapeCamera, color, bits,
|
||||
cam.primary ? 1.3f : 1.0f);
|
||||
EmitFrustum(t, cam, sel ? kGizmoSelectedColor
|
||||
: glm::vec4(0.55f, 0.8f, 1.0f, 0.85f),
|
||||
bits);
|
||||
});
|
||||
|
||||
// 环境组件:位置无关天体的空间锚点标记
|
||||
scene.Each<TransformComponent, SkyboxComponent>(
|
||||
[&](Entity e, TransformComponent& t, SkyboxComponent& sb)
|
||||
{
|
||||
if (!sb.active) return;
|
||||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||
EmitIcon(t.GetWorldPosition(), kShapeSkybox,
|
||||
IsSel(bits) ? kGizmoSelectedColor
|
||||
: glm::vec4(0.45f, 0.55f, 0.9f, 1.0f),
|
||||
bits, 1.0f);
|
||||
});
|
||||
scene.Each<TransformComponent, SkyLightComponent>(
|
||||
[&](Entity e, TransformComponent& t, SkyLightComponent& sl)
|
||||
{
|
||||
if (!sl.active) return;
|
||||
const u32 bits = static_cast<u32>(entt::to_integral(e));
|
||||
EmitIcon(t.GetWorldPosition(), kShapeSkyLight,
|
||||
IsSel(bits) ? kGizmoSelectedColor
|
||||
: glm::vec4(0.5f, 0.75f, 0.95f, 1.0f),
|
||||
bits, 1.0f);
|
||||
});
|
||||
|
||||
if (truncated && !m_GizmoTruncWarned)
|
||||
{
|
||||
m_GizmoTruncWarned = true;
|
||||
ENGINE_WARN("SceneRenderer: editor gizmo vertex budget "
|
||||
"exceeded, clamped");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool SceneRenderer::BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
|
||||
vk::Extent2D targetExtent, f32 dt)
|
||||
{
|
||||
@@ -303,19 +502,34 @@ namespace Engine
|
||||
const f32 aspect = static_cast<f32>(targetExtent.width) /
|
||||
static_cast<f32>(targetExtent.height);
|
||||
ubo.view = camera->GetView(*camTransform);
|
||||
ubo.proj = camera->GetProjection(aspect, kCameraNear, kCameraFar);
|
||||
// 投影参数(模式/fov/near/far/orthoSize)由相机自身承载:
|
||||
// 编辑器形态 = 编辑器相机,游戏形态 = primary 相机实体
|
||||
ubo.proj = camera->GetProjection(aspect);
|
||||
|
||||
// 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity)
|
||||
ubo.projNoJitter = ubo.proj;
|
||||
|
||||
// 子像素抖动(Halton×8):光栅化采样位置逐帧偏移,TAA 累积去锯齿。
|
||||
// NDC 偏移 = 像素偏移 × 2 / 尺寸。TAA 关闭时停抖(直通画面静止)
|
||||
// NDC 偏移 = 像素偏移 × 2 / 尺寸。TAA 关闭时停抖(直通画面静止)。
|
||||
// 抖动分量必须按投影模式选(w_clip 语义不同):
|
||||
// 透视 w_clip = -z_eye → 加 z 列([2][x]),被 w 除后 NDC 偏移
|
||||
// 恒定(z_eye 约掉);正交 w_clip = 1 → z 列偏移会随视图深度
|
||||
// 放大(far=10000 时抖动放大上万倍 = 切正交后画面狂震的根因),
|
||||
// 必须加平移列([3][x]),与 w=1 相乘才是恒定 NDC 偏移
|
||||
if (m_TAAEnabled) {
|
||||
const u32 idx = static_cast<u32>(m_FrameIndex % 8);
|
||||
const f32 jx = Halton(idx + 1, 2) - 0.5f;
|
||||
const f32 jy = Halton(idx + 1, 3) - 0.5f;
|
||||
ubo.proj[2][0] += jx * 2.0f / static_cast<f32>(targetExtent.width);
|
||||
ubo.proj[2][1] += jy * 2.0f / static_cast<f32>(targetExtent.height);
|
||||
const f32 sx = 2.0f / static_cast<f32>(targetExtent.width);
|
||||
const f32 sy = 2.0f / static_cast<f32>(targetExtent.height);
|
||||
if (camera->GetProjectionMode() ==
|
||||
Camera::ProjectionMode::Orthographic) {
|
||||
ubo.proj[3][0] += jx * sx;
|
||||
ubo.proj[3][1] += jy * sy;
|
||||
} else {
|
||||
ubo.proj[2][0] += jx * sx;
|
||||
ubo.proj[2][1] += jy * sy;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 环境组件落帧(UE SkyLight/Skybox Actor 语义):场景环境
|
||||
@@ -353,6 +567,13 @@ namespace Engine
|
||||
m_Frame.lodEnabled = m_LodEnabled;
|
||||
m_Frame.debugView = m_DebugView;
|
||||
m_Frame.displayMode = m_DisplayMode;
|
||||
#ifdef HELLO_EDITOR
|
||||
// 骨骼可视化设置(ImGui 经 Renderer 门面 → 此处落帧)
|
||||
m_Frame.showBoneWires = m_ShowBoneWires;
|
||||
m_Frame.boneAllNodes = m_BoneAllNodes;
|
||||
m_Frame.boneJointPx = m_BoneJointPx;
|
||||
m_Frame.pieActive = m_GameView; // PIE 态骨骼可视化整体停产
|
||||
#endif
|
||||
ubo.cascadeSplits.w = m_ShadowsEnabled ? 1.0f : 0.0f;
|
||||
|
||||
// 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass;
|
||||
@@ -422,111 +643,37 @@ namespace Engine
|
||||
collected[0].params.x < 0.5f) {
|
||||
shadowDir = glm::vec3(collected[0].direction);
|
||||
}
|
||||
ComputeCascades(shadowDir);
|
||||
ComputeCascades(shadowDir, camera->GetNearZ(), camera->GetFarZ());
|
||||
ubo.cameraPos = glm::vec4(camTransform->GetWorldPosition(), 1.0f);
|
||||
|
||||
// ---- 2.5 骨骼动画推进 + 姿态采样(与可见性无关:播放头恒走,
|
||||
// 否则被剔除的实体回屏时动画跳变)----
|
||||
// palette 槽 0 固定 identity:静态模型/无动画实例的蒙皮直通
|
||||
m_Frame.skinPalettes.clear();
|
||||
m_Frame.skinPalettes.push_back(glm::mat4(1.0f));
|
||||
m_Frame.skinOffsets.clear();
|
||||
m_Frame.skinPaletteTruncated = false;
|
||||
m_EntitySkinBase.clear();
|
||||
scene.Each<MeshRendererComponent, AnimationComponent>(
|
||||
[&](Entity entity, MeshRendererComponent& mr, AnimationComponent& anim) {
|
||||
if (!anim.active) return; // 组件禁用 = 暂停采样(播放头不动)
|
||||
if (!mr.model.Valid()) return; // 未指派/手清 = 无效句柄(Get 对无效 FATAL)
|
||||
const Model* model = assets.GetModel(mr.model);
|
||||
if (!model || !model->HasAnimations()) return;
|
||||
const auto& clips = model->GetClips();
|
||||
// ---- 2.5 骨骼动画(调度收口 Animation::AnimationSystem:推进
|
||||
// 播放头/crossfade + 姿态求值 + palette 产出。与可见性无关:
|
||||
// 播放头恒走,否则被剔除的实体回屏时动画跳变)----
|
||||
// 渲染侧零动画逻辑:收集段只查 frame.entitySkinBases(查不到 =
|
||||
// identity 槽 = 非蒙皮静态;蒙皮实体由 AnimationSystem 兜底
|
||||
// rest-pose palette,无动画组件也成立)
|
||||
m_AnimSystem.Update(scene, assets, dt, m_Frame);
|
||||
|
||||
// clip 下标合法性(模型资产可能已换)
|
||||
if (anim.clipIndex < 0 ||
|
||||
anim.clipIndex >= static_cast<int>(clips.size())) {
|
||||
anim.clipIndex = 0;
|
||||
anim.fadeFrom = -1;
|
||||
if (clips.empty()) return;
|
||||
}
|
||||
if (anim.fadeFrom >= static_cast<int>(clips.size())) {
|
||||
anim.fadeFrom = -1;
|
||||
}
|
||||
|
||||
// ---- 播放头推进(wrap/clamp)----
|
||||
if (anim.playing && anim.speed != 0.0f) {
|
||||
const f32 d = dt * anim.speed;
|
||||
anim.time += d;
|
||||
if (anim.fadeFrom >= 0) anim.fadeFromTime += d;
|
||||
}
|
||||
const auto wrapTime = [&](f32 t, const AnimationClip& c) {
|
||||
if (c.duration <= 0.0f) return 0.0f;
|
||||
return anim.loop ? glm::mod(t, c.duration)
|
||||
: glm::clamp(t, 0.0f, c.duration - 1e-3f);
|
||||
};
|
||||
const AnimationClip& cur = clips[anim.clipIndex];
|
||||
anim.time = wrapTime(anim.time, cur);
|
||||
const AnimationClip* src = nullptr;
|
||||
f32 srcTime = 0.0f;
|
||||
f32 wCur = 1.0f; // 当前 clip 权重(crossfade 0→1)
|
||||
if (anim.fadeFrom >= 0) {
|
||||
// 淡出计时(暂停时冻结,与播放头一致)
|
||||
if (anim.playing) anim.fadeElapsed += dt;
|
||||
const f32 f = glm::min(
|
||||
anim.fadeElapsed / glm::max(anim.fadeDuration, 1e-4f),
|
||||
1.0f);
|
||||
wCur = f * f * (3.0f - 2.0f * f); // smoothstep
|
||||
if (f >= 1.0f) {
|
||||
anim.fadeFrom = -1; // 过渡完成
|
||||
#ifdef HELLO_EDITOR
|
||||
// ---- 2.7 Editor gizmo 收集(编辑形态专属;纯 CPU 只读场景)。
|
||||
// billboard right/up 从视图矩阵行向量提取(相机无缩放)----
|
||||
if (m_DisplayMode == kDisplayViewport && !m_GameView) {
|
||||
CollectEditorGizmos(scene, camTransform->GetWorldPosition(),
|
||||
glm::vec3(ubo.view[0][0], ubo.view[1][0], ubo.view[2][0]),
|
||||
glm::vec3(ubo.view[0][1], ubo.view[1][1], ubo.view[2][1]),
|
||||
aspect, static_cast<f32>(targetExtent.height),
|
||||
std::tan(glm::radians(
|
||||
m_EditorCamera.GetController().GetFovDeg()) * 0.5f));
|
||||
} else {
|
||||
src = &clips[anim.fadeFrom];
|
||||
anim.fadeFromTime = wrapTime(anim.fadeFromTime, *src);
|
||||
srcTime = anim.fadeFromTime;
|
||||
m_Frame.gizmoIconVerts.clear();
|
||||
m_Frame.gizmoWireVerts.clear();
|
||||
m_Frame.gizmoProxyVerts.clear();
|
||||
// 骨骼可视化数组与 gizmo 同批清(AnimationSystem.Update 开头
|
||||
// 也会清,此处双保险:PIE 态/全屏直渲绝无残留绘制)
|
||||
m_Frame.boneWireVerts.clear();
|
||||
m_Frame.boneJoints.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 采样 + 局部 TRS 层混合 → global → palette ----
|
||||
SampleLocalPose(*model, cur, anim.time,
|
||||
m_PosePosA, m_PoseRotA, m_PoseSclA, m_TouchedA);
|
||||
if (src) {
|
||||
SampleLocalPose(*model, *src, srcTime,
|
||||
m_PosePosB, m_PoseRotB, m_PoseSclB,
|
||||
m_TouchedB);
|
||||
const size_t n = m_PosePosA.size();
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
// touched = 两层轨道的并集(重建集合)
|
||||
m_TouchedA[i] = static_cast<u8>(
|
||||
m_TouchedA[i] | m_TouchedB[i]);
|
||||
m_PosePosA[i] = glm::mix(m_PosePosB[i], m_PosePosA[i], wCur);
|
||||
m_PoseSclA[i] = glm::mix(m_PoseSclB[i], m_PoseSclA[i], wCur);
|
||||
// 旋转必须 slerp(lerp 单位四元数会走样)
|
||||
m_PoseRotA[i] = glm::slerp(m_PoseRotB[i], m_PoseRotA[i], wCur);
|
||||
}
|
||||
}
|
||||
ComposeGlobals(*model, m_PosePosA, m_PoseRotA, m_PoseSclA,
|
||||
m_TouchedA, m_Globals);
|
||||
|
||||
// palette bump 分配(按实体,不按模型去重——同模型两实体
|
||||
// 可不同播放状态)。预算打满 → 该实体退回绑定姿势
|
||||
const auto& bones = model->GetBones();
|
||||
const u32 base = static_cast<u32>(m_Frame.skinPalettes.size());
|
||||
if (base + bones.size() > kMaxSkinMatrices) {
|
||||
m_Frame.skinPaletteTruncated = true;
|
||||
if (!m_SkinTruncWarned) {
|
||||
m_SkinTruncWarned = true;
|
||||
ENGINE_WARN("GPUDriven: skin palette full ({}), "
|
||||
"animated entities fall back to bind pose",
|
||||
kMaxSkinMatrices);
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const auto& b : bones) {
|
||||
const u32 ni = b.nodeIdx < m_Globals.size()
|
||||
? b.nodeIdx : 0;
|
||||
m_Frame.skinPalettes.push_back(m_Globals[ni] * b.invBind);
|
||||
}
|
||||
const u32 entKey = static_cast<u32>(entt::to_integral(entity));
|
||||
m_EntitySkinBase[entKey] = base;
|
||||
});
|
||||
#endif
|
||||
|
||||
// ---- 3. GPUDriven:单趟收集 + 多视图剔除 ----
|
||||
// 同作用域第二个 zone:ZoneScopedN 变量名固定会重声明,用 ZoneNamedN
|
||||
@@ -576,10 +723,12 @@ namespace Engine
|
||||
}
|
||||
m_CurWorlds.emplace_back(entKey, t.World());
|
||||
|
||||
// palette 基址(第 2.5 节预采样;无动画实体 = identity 槽)
|
||||
// palette 基址(AnimationSystem 预采样:动画实体=当帧
|
||||
// 姿态、蒙皮无动画实体=rest 兜底;查不到=identity 槽,
|
||||
// 只可能是非蒙皮静态模型)
|
||||
u32 skinBase = kIdentityPaletteSlot;
|
||||
if (const auto it = m_EntitySkinBase.find(entKey);
|
||||
it != m_EntitySkinBase.end()) {
|
||||
if (const auto it = m_Frame.entitySkinBases.find(entKey);
|
||||
it != m_Frame.entitySkinBases.end()) {
|
||||
skinBase = it->second;
|
||||
}
|
||||
|
||||
@@ -606,12 +755,38 @@ namespace Engine
|
||||
}
|
||||
if (!anyVis) return;
|
||||
|
||||
for (auto& mesh : model->GetMeshes()) {
|
||||
const auto& meshes = model->GetMeshes();
|
||||
for (size_t meshIdx = 0; meshIdx < meshes.size(); ++meshIdx) {
|
||||
auto& mesh = meshes[meshIdx];
|
||||
const auto& bounds = mesh->GetBounds();
|
||||
const glm::vec3 worldCenter =
|
||||
glm::vec3(t.World() * glm::vec4(bounds.center, 1.0f));
|
||||
const f32 worldRadius = bounds.radius * worldScale;
|
||||
|
||||
// ---- 实体级材质覆盖 resolve(UE Material Slot)----
|
||||
// 覆盖路径命中已加载资产 → 用其 GPU 材质;未加载/
|
||||
// 解析失败回退内建(一次性告警)。收集期只查缓存
|
||||
// 不建 GPU 资源——加载点收敛在场景预载(游戏形态
|
||||
// 唯一加载点)与编辑器指派(UI 只是编辑接口)
|
||||
Material* resolved = mesh->GetMaterial();
|
||||
if (!mr.materialOverrides.empty()) {
|
||||
if (const auto oIt = mr.materialOverrides.find(
|
||||
static_cast<u32>(meshIdx));
|
||||
oIt != mr.materialOverrides.end()) {
|
||||
if (MaterialAsset* asset =
|
||||
assets.FindMaterialAsset(oIt->second);
|
||||
asset != nullptr) {
|
||||
resolved = asset->GetMaterial();
|
||||
}
|
||||
else if (m_WarnedMaterialMisses.insert(
|
||||
oIt->second).second) {
|
||||
ENGINE_WARN("GPUDriven: material asset "
|
||||
"'{}' not loaded, falling back "
|
||||
"to built-in", oIt->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LOD 选级:屏占比近似 = radius/距离(主视图按实例距离;
|
||||
// 阈值 10/20 倍半径,可调)。开关关闭 = 恒 LOD0
|
||||
const f32 dist =
|
||||
@@ -634,20 +809,38 @@ namespace Engine
|
||||
instanceTruncated = true;
|
||||
return;
|
||||
}
|
||||
// batch 查找 = 数组寻址(Mesh::GetSlot 槽位 + LOD 槽)
|
||||
// batchIdx 存储 +1 偏移(0 = 该槽位尚无 batch)
|
||||
// batch 查找:内建材质 = 数组寻址快路径
|
||||
//(Mesh::GetSlot 槽位 + LOD 槽);覆盖材质 =
|
||||
// hash 寻址(键含材质变体:同 mesh 不同覆盖天然
|
||||
// 分批,同覆盖多实体仍合并——材质资产指针仅帧内
|
||||
// 有效,变体表每帧随 ViewScratch::Clear 清空)
|
||||
const bool builtin = (resolved == mesh->GetMaterial());
|
||||
std::array<u32, kMeshLodCount>* lodSlots = nullptr;
|
||||
if (builtin) {
|
||||
const u32 slot = mesh->GetSlot();
|
||||
if (slot >= s.lookup.size()) {
|
||||
s.lookup.resize(slot + 1);
|
||||
}
|
||||
auto& lodSlots = s.lookup[slot];
|
||||
u32 batchIdx = lodSlots[lod];
|
||||
lodSlots = &s.lookup[slot];
|
||||
}
|
||||
else {
|
||||
const auto [vIt, inserted] = s.matVariants.emplace(
|
||||
resolved,
|
||||
static_cast<u32>(s.matVariants.size()));
|
||||
const u64 key =
|
||||
(static_cast<u64>(mesh->GetSlot()) << 32) |
|
||||
static_cast<u64>(vIt->second);
|
||||
lodSlots = &s.ovrLookup[key];
|
||||
}
|
||||
// batchIdx 存储 +1 偏移(0 = 该槽位尚无 batch)
|
||||
u32 batchIdx = (*lodSlots)[lod];
|
||||
if (batchIdx == 0) {
|
||||
batchIdx = static_cast<u32>(s.result.batches.size()) + 1;
|
||||
lodSlots[lod] = batchIdx;
|
||||
(*lodSlots)[lod] = batchIdx;
|
||||
// first 是占位:真实基址在 appendView 聚合时
|
||||
// 回填(收集序下 batch 实例散布,见链表注释)
|
||||
s.result.batches.push_back({mesh.get(), 0, 0, lod});
|
||||
s.result.batches.push_back(
|
||||
{mesh.get(), resolved, 0, 0, lod});
|
||||
s.batchDistSum.push_back(0.0f);
|
||||
s.batchHead.push_back(ViewScratch::kNoInstance);
|
||||
}
|
||||
@@ -668,9 +861,10 @@ namespace Engine
|
||||
};
|
||||
|
||||
// 透明材质 → 独立队列(主视图,混合绘制 + 排序;
|
||||
// 不进阴影——alpha 阴影复杂度高收益低,注释后续演进)
|
||||
const bool isTransparentMesh =
|
||||
mesh->GetMaterial()->IsTransparent();
|
||||
// 不进阴影——alpha 阴影复杂度高收益低,注释后续演进)。
|
||||
// 队列归属按 resolve 后的实际材质:同一 mesh 可因
|
||||
// 覆盖不同而分属不透明/透明两队列
|
||||
const bool isTransparentMesh = resolved->IsTransparent();
|
||||
ViewScratch& mainTarget = isTransparentMesh
|
||||
? m_TranslucentScratch : m_MainScratch;
|
||||
|
||||
@@ -851,16 +1045,16 @@ namespace Engine
|
||||
const u64 vb = reinterpret_cast<u64>(static_cast<VkBuffer>(
|
||||
b.mesh->GetVertexBuffer()));
|
||||
if (va != vb) return va < vb;
|
||||
if (byMaterial && a.mesh->GetMaterial() != b.mesh->GetMaterial()) {
|
||||
return reinterpret_cast<u64>(a.mesh->GetMaterial()) <
|
||||
reinterpret_cast<u64>(b.mesh->GetMaterial());
|
||||
if (byMaterial && a.material != b.material) {
|
||||
return reinterpret_cast<u64>(a.material) <
|
||||
reinterpret_cast<u64>(b.material);
|
||||
}
|
||||
return a.mesh->GetSlot() < b.mesh->GetSlot();
|
||||
});
|
||||
|
||||
for (const auto& b : view.batches) {
|
||||
const vk::Buffer vb = b.mesh->GetVertexBuffer();
|
||||
Material* mat = byMaterial ? b.mesh->GetMaterial() : nullptr;
|
||||
Material* mat = byMaterial ? b.material : nullptr;
|
||||
|
||||
bool newGroup = view.indirectGroups.empty();
|
||||
if (!newGroup) {
|
||||
|
||||
@@ -5,9 +5,67 @@
|
||||
#include <Engine/Renderer/VulkanAssetUploader.h>
|
||||
#include <Engine/Renderer/Model.h>
|
||||
#include <Engine/Renderer/IblGenerator.h>
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/Texture.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
VulkanAssetUploader::~VulkanAssetUploader() = default;
|
||||
|
||||
void VulkanAssetUploader::InitSharedDefaultTextures()
|
||||
{
|
||||
if (m_DefaultTextures[0]) return; // 已初始化
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
m_DefaultTextures[slot] = CreateDefaultMaterialTexture(
|
||||
m_Device, m_Allocator, static_cast<Material::Slot>(slot));
|
||||
}
|
||||
}
|
||||
|
||||
Scope<MaterialAsset> VulkanAssetUploader::CreateMaterial(
|
||||
MaterialAssetData&& data)
|
||||
{
|
||||
InitSharedDefaultTextures();
|
||||
|
||||
// 每槽解析:路径(CWD 相对,反斜杠归一,与 Model::GetOrLoadTexture
|
||||
// 同语义)→ 独占纹理;空/缺失 → 共享默认纹理(非拥有)
|
||||
std::array<Scope<Texture>, Material::kSlotCount> owned;
|
||||
std::array<Texture*, Material::kSlotCount> bound{};
|
||||
// 槽位 sRGB 语义:颜色图 true,数据图 false(与 Model 同表)
|
||||
const bool slotSrgb[Material::kSlotCount] = {true, false, false,
|
||||
true, false, false};
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
std::string path = data.texPaths[slot];
|
||||
std::replace(path.begin(), path.end(), '\\', '/');
|
||||
if (!path.empty() && std::filesystem::exists(path)) {
|
||||
owned[slot] = CreateScope<Texture>(
|
||||
m_Device, m_Allocator, path, slotSrgb[slot]);
|
||||
bound[slot] = owned[slot].get();
|
||||
}
|
||||
else {
|
||||
if (!path.empty()) {
|
||||
ENGINE_WARN("MaterialAsset: texture not found '{}', "
|
||||
"slot {} falls back to default", path, slot);
|
||||
}
|
||||
bound[slot] = m_DefaultTextures[slot].get();
|
||||
}
|
||||
}
|
||||
|
||||
auto mat = CreateScope<Material>(
|
||||
m_Device, m_Allocator, m_Descriptors, m_TextureLayout,
|
||||
bound, data.params, data.transparent);
|
||||
return CreateScope<MaterialAsset>(std::move(data), std::move(owned),
|
||||
std::move(mat));
|
||||
}
|
||||
|
||||
void VulkanAssetUploader::WaitDeviceIdle()
|
||||
{
|
||||
m_Device.GetDevice().waitIdle();
|
||||
}
|
||||
|
||||
Scope<Model> VulkanAssetUploader::LoadModel(const std::string& path)
|
||||
{
|
||||
// GPU 上传在此发生:解析(assimp)+ buffer/image 创建一步完成
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// VulkanAssetUploader:IAssetUploader 的渲染层实现。
|
||||
//
|
||||
// 属于渲染层对资产层提供的服务:持有 GPU 上下文,负责把解析后的
|
||||
// 资产数据创建为 Vulkan 资源。资产层(AssetManager)只看到接口,
|
||||
// 不知道 Device/Allocator 的存在——依赖方向:Assets → IAssetUploader
|
||||
// ← VulkanAssetUploader(渲染层),永远不出现 Assets → Vulkan 具体。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_VULKANASSETUPLOADER_H
|
||||
#define HELLOVULKAN_VULKANASSETUPLOADER_H
|
||||
|
||||
#include <Engine/Assets/IAssetUploader.h>
|
||||
|
||||
namespace Engine {
|
||||
|
||||
class Device;
|
||||
class MemoryAllocator;
|
||||
class DescriptorManager;
|
||||
class IblGenerator;
|
||||
|
||||
class VulkanAssetUploader final : public Assets::IAssetUploader {
|
||||
public:
|
||||
VulkanAssetUploader(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptors,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
IblGenerator& ibl)
|
||||
: m_Device(device), m_Allocator(allocator),
|
||||
m_Descriptors(descriptors), m_TextureLayout(textureLayout),
|
||||
m_Ibl(ibl) {}
|
||||
|
||||
Scope<Model> LoadModel(const std::string& path) override;
|
||||
Scope<Model> LoadModel(const std::string& path,
|
||||
ParsedModel&& parsed) override;
|
||||
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override;
|
||||
Scope<Model> CreateTransparentPlane(f32 halfSize,
|
||||
u8 r, u8 g, u8 b, u8 a) override;
|
||||
Scope<Cubemap> LoadEnvironment(const std::string& path) override;
|
||||
Scope<Cubemap> LoadEnvironment(const std::string& path,
|
||||
DecodedEquirect&& decoded) override;
|
||||
Scope<MaterialAsset> CreateMaterial(
|
||||
MaterialAssetData&& data) override;
|
||||
void WaitDeviceIdle() override;
|
||||
|
||||
private:
|
||||
// 惰性创建的共享默认纹理集(空槽/加载失败兜底;全体材质资产
|
||||
// 共用一份,非拥有引用进各 MaterialAsset)
|
||||
void InitSharedDefaultTextures();
|
||||
|
||||
Device& m_Device;
|
||||
MemoryAllocator& m_Allocator;
|
||||
DescriptorManager& m_Descriptors;
|
||||
vk::DescriptorSetLayout m_TextureLayout;
|
||||
IblGenerator& m_Ibl; // 环境 IBL 预积分的 compute 生成端
|
||||
|
||||
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures;
|
||||
};
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
#endif //HELLOVULKAN_VULKANASSETUPLOADER_H
|
||||
@@ -169,6 +169,25 @@ namespace Engine::Serialization {
|
||||
return ref;
|
||||
}
|
||||
|
||||
// 材质覆盖路径解析:guid 反查优先(资产移动不断引用),path
|
||||
// 兜底;解析成功即预载——"收集期只查缓存"不变量在此保证
|
||||
//(游戏形态无编辑器,场景读后预载是唯一自动加载点),未加载
|
||||
// 成功也返回路径(收集期回退内建材质,不丢实体)
|
||||
std::string ResolveMaterialPath(Assets::AssetManager& assets,
|
||||
u64 guid, const std::string& path)
|
||||
{
|
||||
std::string resolved;
|
||||
if (guid != 0) resolved = assets.ResolveGUID(guid);
|
||||
if (resolved.empty()) resolved = path;
|
||||
if (resolved.empty()) return {};
|
||||
|
||||
if (!assets.IsMaterialLoaded(resolved)) {
|
||||
assets.LoadMaterialAsset(resolved);
|
||||
}
|
||||
assets.GetOrCreateGUID(resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// ---- 中间结构:解析全部成功才碰 registry ----
|
||||
// comps 持 Components 节点(仅在键存在且为 map 时赋值——
|
||||
// yaml-cpp 新版对僵尸节点做 operator= 会抛异常)
|
||||
@@ -229,6 +248,25 @@ namespace Engine::Serialization {
|
||||
out << YAML::Key << "Path" << YAML::Value << path;
|
||||
out << YAML::EndMap; // Model(flow)
|
||||
out << YAML::Key << "Active" << YAML::Value << mr->active;
|
||||
// 材质覆盖表(UE Material Slot):seq of {Slot,Guid,Path}
|
||||
if (!mr->materialOverrides.empty()) {
|
||||
out << YAML::Key << "MaterialOverrides"
|
||||
<< YAML::Value << YAML::BeginSeq;
|
||||
for (const auto& [slot, mpath] :
|
||||
mr->materialOverrides) {
|
||||
out << YAML::BeginMap;
|
||||
out << YAML::Key << "Slot" << YAML::Value << slot;
|
||||
if (const u64 guid = assets.GetOrCreateGUID(mpath);
|
||||
guid != 0) {
|
||||
out << YAML::Key << "Guid"
|
||||
<< YAML::Value << guid;
|
||||
}
|
||||
out << YAML::Key << "Path"
|
||||
<< YAML::Value << mpath;
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
out << YAML::EndSeq;
|
||||
}
|
||||
out << YAML::EndMap; // MeshRenderer
|
||||
}
|
||||
else {
|
||||
@@ -244,7 +282,31 @@ namespace Engine::Serialization {
|
||||
out << YAML::Key << "Playing" << YAML::Value << a->playing;
|
||||
out << YAML::Key << "Loop" << YAML::Value << a->loop;
|
||||
out << YAML::Key << "Speed" << YAML::Value << a->speed;
|
||||
out << YAML::Key << "FadeDuration" << YAML::Value
|
||||
<< a->fadeDuration;
|
||||
out << YAML::Key << "Active" << YAML::Value << a->active;
|
||||
// Controller 模式(Unity Animator 语义):mode/路径/参数
|
||||
// 实例值落盘;状态机播放头瞬态不落盘(重启从 entry 开始)
|
||||
out << YAML::Key << "Mode" << YAML::Value
|
||||
<< static_cast<int>(a->mode);
|
||||
out << YAML::Key << "ControllerPath" << YAML::Value
|
||||
<< a->controllerPath;
|
||||
if (!a->floatParams.empty()) {
|
||||
out << YAML::Key << "FloatParams" << YAML::Value
|
||||
<< YAML::BeginMap;
|
||||
for (const auto& [k, v] : a->floatParams) {
|
||||
out << YAML::Key << k << YAML::Value << v;
|
||||
}
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
if (!a->boolParams.empty()) {
|
||||
out << YAML::Key << "BoolParams" << YAML::Value
|
||||
<< YAML::BeginMap;
|
||||
for (const auto& [k, v] : a->boolParams) {
|
||||
out << YAML::Key << k << YAML::Value << v;
|
||||
}
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
// 外部动画源(编辑器 attach 列表):clip 本体运行时并入
|
||||
// Model 资产不落盘,加载端按此列表幂等重放
|
||||
out << YAML::Key << "Attached" << YAML::Value << YAML::Flow
|
||||
@@ -261,6 +323,17 @@ namespace Engine::Serialization {
|
||||
<< YAML::BeginMap;
|
||||
out << YAML::Key << "Primary" << YAML::Value << c->primary;
|
||||
out << YAML::Key << "Active" << YAML::Value << c->active;
|
||||
// 投影参数(Camera 成员):老场景缺键时读侧回退默认值
|
||||
out << YAML::Key << "ProjectionMode" << YAML::Value
|
||||
<< static_cast<int>(c->camera.GetProjectionMode());
|
||||
out << YAML::Key << "FovDeg" << YAML::Value
|
||||
<< c->camera.GetFovDeg();
|
||||
out << YAML::Key << "NearZ" << YAML::Value
|
||||
<< c->camera.GetNearZ();
|
||||
out << YAML::Key << "FarZ" << YAML::Value
|
||||
<< c->camera.GetFarZ();
|
||||
out << YAML::Key << "OrthoSize" << YAML::Value
|
||||
<< c->camera.GetOrthoSize();
|
||||
out << YAML::EndMap;
|
||||
}
|
||||
|
||||
@@ -598,8 +671,25 @@ namespace Engine::Serialization {
|
||||
ref.Valid()) {
|
||||
handle.AddComponent<ECS::MeshRendererComponent>(
|
||||
ECS::MeshRendererComponent{ref});
|
||||
handle.GetComponent<ECS::MeshRendererComponent>().active =
|
||||
auto& mrComp =
|
||||
handle.GetComponent<ECS::MeshRendererComponent>();
|
||||
mrComp.active =
|
||||
n["Active"].as<bool>(true); // 缺省 true 向前兼容
|
||||
// 材质覆盖表:逐项解析(guid 反查 → path 兜底)
|
||||
// 并预载;加载失败保留路径(收集期回退内建材质)
|
||||
if (const YAML::Node ovs = n["MaterialOverrides"]) {
|
||||
for (const auto& ov : ovs) {
|
||||
const u32 slot = ov["Slot"].as<u32>(
|
||||
0xFFFFFFFFu);
|
||||
const auto opath = ResolveMaterialPath(
|
||||
assets, ov["Guid"].as<u64>(0),
|
||||
ov["Path"].as<std::string>(""));
|
||||
if (slot == 0xFFFFFFFFu || opath.empty()) {
|
||||
continue;
|
||||
}
|
||||
mrComp.materialOverrides[slot] = opath;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
ENGINE_WARN("SceneSerializer: model unresolved "
|
||||
@@ -616,7 +706,33 @@ namespace Engine::Serialization {
|
||||
a.playing = n["Playing"].as<bool>(a.playing);
|
||||
a.loop = n["Loop"].as<bool>(a.loop);
|
||||
a.speed = n["Speed"].as<f32>(a.speed);
|
||||
a.fadeDuration =
|
||||
n["FadeDuration"].as<f32>(a.fadeDuration); // 缺键回退 0.3
|
||||
a.active = n["Active"].as<bool>(true);
|
||||
// Controller 模式缺键回退 Direct(老场景零迁移);
|
||||
// mode 越界值也视为 Direct
|
||||
if (const int m = n["Mode"].as<int>(0); m == 1) {
|
||||
a.mode = ECS::AnimationComponent::AnimMode::Controller;
|
||||
}
|
||||
a.controllerPath = n["ControllerPath"].as<std::string>("");
|
||||
// Controller 预载(收集期只查缓存的不变量在此保证;
|
||||
// 解析失败保留路径,System Find 落空 → rest 兜底)
|
||||
if (!a.controllerPath.empty() &&
|
||||
!assets.IsControllerLoaded(a.controllerPath)) {
|
||||
assets.LoadController(a.controllerPath);
|
||||
}
|
||||
if (const YAML::Node fp = n["FloatParams"]) {
|
||||
for (const auto& it : fp) {
|
||||
a.floatParams[it.first.as<std::string>("")] =
|
||||
it.second.as<float>(0.0f);
|
||||
}
|
||||
}
|
||||
if (const YAML::Node bp = n["BoolParams"]) {
|
||||
for (const auto& it : bp) {
|
||||
a.boolParams[it.first.as<std::string>("")] =
|
||||
it.second.as<bool>(false);
|
||||
}
|
||||
}
|
||||
if (const YAML::Node att = n["Attached"]) {
|
||||
for (const auto& it : att) {
|
||||
a.attachedAnimPaths.push_back(
|
||||
@@ -629,6 +745,18 @@ namespace Engine::Serialization {
|
||||
auto& c = handle.AddComponent<ECS::CameraComponent>();
|
||||
c.primary = n["Primary"].as<bool>(false);
|
||||
c.active = n["Active"].as<bool>(true);
|
||||
// 投影参数缺省回退默认值(Version 向后兼容:老场景无新键
|
||||
// 可载)。ProjectionMode 越界值视为透视
|
||||
if (const int pm = n["ProjectionMode"].as<int>(0);
|
||||
pm == 1) {
|
||||
c.camera.SetProjectionMode(
|
||||
Engine::Camera::ProjectionMode::Orthographic);
|
||||
}
|
||||
c.camera.SetFovDeg(n["FovDeg"].as<f32>(c.camera.GetFovDeg()));
|
||||
c.camera.SetClip(n["NearZ"].as<f32>(c.camera.GetNearZ()),
|
||||
n["FarZ"].as<f32>(c.camera.GetFarZ()));
|
||||
c.camera.SetOrthoSize(
|
||||
n["OrthoSize"].as<f32>(c.camera.GetOrthoSize()));
|
||||
}
|
||||
|
||||
if (const YAML::Node n = comps["Light"]) {
|
||||
|
||||
@@ -72,6 +72,27 @@ namespace Engine
|
||||
{r, g, b, a}, srgb));
|
||||
}
|
||||
|
||||
Scope<Texture> Texture::CreateFromEncodedMemory(
|
||||
Device& device, MemoryAllocator& allocator,
|
||||
const void* data, u32 bytes, bool srgb)
|
||||
{
|
||||
int width = 0, height = 0, channels = 0;
|
||||
stbi_uc* pixels = stbi_load_from_memory(
|
||||
static_cast<const stbi_uc*>(data), static_cast<int>(bytes),
|
||||
&width, &height, &channels, STBI_rgb_alpha);
|
||||
if (!pixels) {
|
||||
return {};
|
||||
}
|
||||
// 两段式:ProceduralTag ctor 先落 1x1 占位(确定 device/allocator
|
||||
// 成员),随即 InitFromPixels 重走公共上传路径传真实像素
|
||||
Scope<Texture> tex(new Texture(device, allocator, ProceduralTag{},
|
||||
{0, 0, 0, 255}, srgb));
|
||||
tex->InitFromPixels(pixels, static_cast<u32>(width),
|
||||
static_cast<u32>(height), srgb);
|
||||
stbi_image_free(pixels);
|
||||
return tex;
|
||||
}
|
||||
|
||||
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb)
|
||||
{
|
||||
m_Width = width;
|
||||
|
||||
@@ -31,4 +31,10 @@ namespace Engine {
|
||||
m_Buffers[frameSlot]->Upload(data, size);
|
||||
}
|
||||
|
||||
void UniformBuffer::UpdateAt(const u32 frameSlot, const u64 offset,
|
||||
const void* data, const u64 size) const
|
||||
{
|
||||
m_Buffers[frameSlot]->Upload(data, size, offset);
|
||||
}
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#version 450
|
||||
|
||||
// Editor gizmo 图标片元:按 aux.x(形状 id,与 SceneRenderer 收集常量
|
||||
// 一一对应)在 uv ∈ [-1,1] 上做程序化 SDF 造型,预乘 alpha 输出
|
||||
//(pass 侧混合 ONE, ONE_MINUS_SRC_ALPHA)。
|
||||
// 0 方向光(太阳盘 + 8 向光芒)
|
||||
// 1 点光(亮核 + 柔和光晕)
|
||||
// 2 相机(机身挖孔 + 镜头环 + 取景器凸块)
|
||||
// 3 天空盒(等距立方线框)
|
||||
// 4 天空光(环 + 内核点)
|
||||
|
||||
layout(location = 0) in vec4 inColor;
|
||||
layout(location = 1) in vec4 inAux;
|
||||
|
||||
layout(location = 0) out vec4 outColor;
|
||||
|
||||
// 方框 SDF
|
||||
float sdBox(vec2 p, vec2 b) {
|
||||
vec2 d = abs(p) - b;
|
||||
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
|
||||
}
|
||||
|
||||
// 线段 SDF
|
||||
float sdSegment(vec2 p, vec2 a, vec2 b) {
|
||||
vec2 pa = p - a, ba = b - a;
|
||||
float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
|
||||
return length(pa - ba * h);
|
||||
}
|
||||
|
||||
// 等边六边形 SDF(circumradius r,iq 惯用式)
|
||||
float sdHexagon(vec2 p, float r) {
|
||||
const vec3 k = vec3(-0.866025404, 0.5, 0.577350269);
|
||||
p = abs(p);
|
||||
p -= 2.0 * min(dot(k.xy, p), 0.0) * k.xy;
|
||||
p -= vec2(clamp(p.x, -k.z * r, k.z * r), r);
|
||||
return length(p) * sign(p.y);
|
||||
}
|
||||
|
||||
float ShapeSDF(vec2 p, int id) {
|
||||
if (id == 0) { // 方向光:太阳盘 + 8 向光芒
|
||||
float r = length(p);
|
||||
float disc = r - 0.38;
|
||||
float a = atan(p.y, p.x);
|
||||
float s45 = 0.78539816; // 45° 扇区
|
||||
float fa = abs(mod(a + s45 * 0.5, s45) - s45 * 0.5);
|
||||
float ray = max(r * sin(fa), abs(r - 0.58) - 0.20);
|
||||
return min(disc, ray);
|
||||
}
|
||||
if (id == 1) return length(p) - 0.22; // 点光亮核(光晕在 main)
|
||||
if (id == 2) { // 相机
|
||||
float body = sdBox(p - vec2(0.05, 0.0), vec2(0.50, 0.36));
|
||||
float hole = max(body, -(length(p - vec2(0.05, 0.0)) - 0.20));
|
||||
float ring = abs(length(p - vec2(0.05, 0.0)) - 0.28) - 0.035;
|
||||
float vf = sdBox(p - vec2(0.30, 0.44), vec2(0.14, 0.10));
|
||||
return min(min(hole, ring), vf);
|
||||
}
|
||||
if (id == 3) { // 天空盒:等距立方线框
|
||||
float hex = abs(sdHexagon(p, 0.72)) - 0.045;
|
||||
float e1 = sdSegment(p, vec2(0.0), vec2(0.0, -0.72)) - 0.045;
|
||||
float e2 = sdSegment(p, vec2(0.0), vec2(-0.6235, 0.36)) - 0.045;
|
||||
float e3 = sdSegment(p, vec2(0.0), vec2(0.6235, 0.36)) - 0.045;
|
||||
return min(hex, min(e1, min(e2, e3)));
|
||||
}
|
||||
// 天空光(id == 4):环 + 内核点
|
||||
return min(abs(length(p) - 0.52) - 0.09, length(p) - 0.14);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = inAux.yz;
|
||||
int id = int(inAux.x + 0.5);
|
||||
// uv 空间的像素级边缘宽度(quad 上 uv 随屏幕像素线性变化)
|
||||
float aa = fwidth(uv.x) * 1.5 + 1e-4;
|
||||
|
||||
float alpha = smoothstep(aa, -aa, ShapeSDF(uv, id));
|
||||
|
||||
if (id == 1) { // 点光:核心外叠加径向光晕
|
||||
float r = length(uv);
|
||||
float halo = (1.0 - smoothstep(0.25, 1.0, r)) * 0.35;
|
||||
alpha = max(alpha, halo);
|
||||
}
|
||||
|
||||
// 预乘 alpha:ONE / ONE_MINUS_SRC_ALPHA 混合下的正确形式
|
||||
outColor = vec4(inColor.rgb * alpha, alpha);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#version 450
|
||||
|
||||
// Editor gizmo billboard 顶点:世界空间 billboard quad(CPU 侧已面向相机
|
||||
// 展开),无描述符 set——viewProj 走 push constant(与 debug_line 同约定)。
|
||||
// aux = (shapeId, uv.x, uv.y, 0),uv ∈ [-1,1] 供片元 SDF 造型。
|
||||
|
||||
layout(location = 0) in vec3 inPos;
|
||||
layout(location = 1) in vec4 inColor;
|
||||
layout(location = 2) in vec4 inAux;
|
||||
|
||||
layout(location = 0) out vec4 outColor;
|
||||
layout(location = 1) out vec4 outAux;
|
||||
|
||||
layout(push_constant) uniform PC {
|
||||
mat4 viewProj;
|
||||
} pc;
|
||||
|
||||
void main() {
|
||||
gl_Position = pc.viewProj * vec4(inPos, 1.0);
|
||||
outColor = inColor;
|
||||
outAux = inAux;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#version 450
|
||||
|
||||
// Editor gizmo 拾取片元:实体 id 原样写 R32Uint(与 hitproxy.frag 同
|
||||
// 语义)。flat = 无插值,像素值即命中图元的实体位模式。
|
||||
|
||||
layout(location = 0) flat in uint inEntityId;
|
||||
|
||||
layout(location = 0) out uvec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = uvec4(inEntityId, 0u, 0u, 0u);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#version 450
|
||||
|
||||
// Editor gizmo 拾取顶点:与 billboard 共享同一 ring VB(pos/color/aux
|
||||
// + entityId 四属性流),画进 HitProxy 的 R32Uint 目标。只有 entityId
|
||||
// 参与变换输出,color/aux 声明占位(属性流绑定时全开)。
|
||||
|
||||
layout(location = 0) in vec3 inPos;
|
||||
layout(location = 1) in vec4 inColor; // 占位(拾取不用)
|
||||
layout(location = 2) in vec4 inAux; // 占位
|
||||
layout(location = 3) in uint inEntityId;
|
||||
|
||||
layout(location = 0) flat out uint outEntityId;
|
||||
|
||||
layout(push_constant) uniform PC {
|
||||
mat4 viewProj;
|
||||
} pc;
|
||||
|
||||
void main() {
|
||||
gl_Position = pc.viewProj * vec4(inPos, 1.0);
|
||||
outEntityId = inEntityId;
|
||||
}
|
||||
@@ -44,8 +44,21 @@ layout(set = 1, binding = 5) uniform sampler2D roughnessMap; // 独立粗糙度
|
||||
layout(set = 1, binding = 6) uniform MaterialParams {
|
||||
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
||||
vec4 baseColor; // glTF baseColorFactor:rgb 调色 albedo,a = BLEND 透明度
|
||||
vec4 surface; // x = ior(F0 换算),y = transmission(不透明段恒 0 语义),z = 自发光强度,w = 体积厚度
|
||||
vec4 misc; // x = unlit(KHR_materials_unlit,1 = 早退直出 albedo)
|
||||
vec4 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22),槽序 = binding
|
||||
vec4 uvT[6]; // 每槽 UV 平移 (tx, ty, 0, 0)
|
||||
} matParams;
|
||||
|
||||
// 每槽 UV 变换:uv' = A·uv + t(identity 时退化为直通)。slot 必须
|
||||
// 字面量调用点(编译期展开索引);6 = Material::kSlotCount
|
||||
vec2 ApplyUv(int slot, vec2 uv) {
|
||||
vec4 a = matParams.uvA[slot];
|
||||
vec4 t = matParams.uvT[slot];
|
||||
return vec2(a.x * uv.x + a.y * uv.y + t.x,
|
||||
a.z * uv.x + a.w * uv.y + t.y);
|
||||
}
|
||||
|
||||
// CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF)
|
||||
layout(set = 2, binding = 0) uniform sampler2DShadow shadowMap0; // CSM 级联 0(最近)
|
||||
layout(set = 2, binding = 1) uniform sampler2DShadow shadowMap1; // 级联 1
|
||||
@@ -136,23 +149,40 @@ vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 albedoTex = texture(albedoMap, fragUV);
|
||||
vec4 albedoTex = texture(albedoMap, ApplyUv(0, fragUV));
|
||||
// alphaMode=MASK 镂空(表针/栅栏等硬边贴图):低于阈值的纹素直接丢弃
|
||||
if (matParams.factors.w > 0.5 && albedoTex.a < 0.5) discard;
|
||||
vec3 albedo = albedoTex.rgb * matParams.baseColor.rgb;
|
||||
vec4 mr = texture(metallicRoughnessMap, fragUV);
|
||||
// KHR_materials_unlit:无光照语义(文字标签/示意图),albedo 直出
|
||||
if (matParams.misc.x > 0.5) {
|
||||
outColor = vec4(albedo, albedoTex.a * matParams.baseColor.a);
|
||||
outVelocity = fragVelocity;
|
||||
return;
|
||||
}
|
||||
vec4 mr = texture(metallicRoughnessMap, ApplyUv(2, fragUV));
|
||||
// 粗糙度源按 factors.z 切换:glTF 合图工作流读 MR.g;
|
||||
// FBX/OBJ specular 工作流(z=1)换用独立粗糙度图 R 通道
|
||||
float roughness = clamp(mix(mr.g, texture(roughnessMap, fragUV).r,
|
||||
float roughness = clamp(mix(mr.g, texture(roughnessMap, ApplyUv(5, fragUV)).r,
|
||||
matParams.factors.z) * matParams.factors.y,
|
||||
0.045, 1.0);
|
||||
float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0);
|
||||
vec3 emissive = texture(emissiveMap, fragUV).rgb;
|
||||
// 自发光强度(KHR_materials_emissive_strength,缺省 1)
|
||||
vec3 emissive = texture(emissiveMap, ApplyUv(3, fragUV)).rgb * matParams.surface.z;
|
||||
|
||||
vec3 N = MappedNormal(normalize(fragNormal), fragTangent, fragUV);
|
||||
vec3 N = MappedNormal(normalize(fragNormal), fragTangent, ApplyUv(1, fragUV));
|
||||
vec3 V = normalize(ubo.cameraPos.xyz - fragWorldPos);
|
||||
// 双面法线:cull 关闭 + glTF doubleSided=true,背面片元(表壳后盖/
|
||||
// 内壁等从背面看到的表面)几何法线背向相机,NdotL≤0 → 直接光全灭、
|
||||
// IBL 采错半球 = 纯黑壳(three.js DoubleSide 对背面自动翻法线,
|
||||
// 这里对齐该语义;与 translucent shader 的翻转同款)
|
||||
if (dot(N, V) < 0.0) N = -N;
|
||||
float NdotV = max(dot(N, V), 1e-4);
|
||||
vec3 F0 = mix(vec3(0.04), albedo, metallic); // 电介质基准反射率 4%
|
||||
// F0 按折射率换算(KHR_materials_ior):((ior-1)/(ior+1))²;
|
||||
// glTF 缺省 ior=1.5 → 0.04,与旧固定值一致
|
||||
float ior = max(matParams.surface.x, 1.0);
|
||||
vec3 F0 = mix(vec3((ior - 1.0) / (ior + 1.0)
|
||||
* (ior - 1.0) / (ior + 1.0)),
|
||||
albedo, metallic); // 电介质 F0 / 金属 albedo
|
||||
|
||||
vec3 Lo = vec3(0.0);
|
||||
float shade0 = 1.0f; // 第 0 盏光的阴影因子(环境光同步衰减用)
|
||||
@@ -213,7 +243,7 @@ void main() {
|
||||
vec3 kD_amb = (1.0 - F_amb) * (1.0 - metallic);
|
||||
|
||||
vec3 irradiance = texture(irradianceMap, N).rgb;
|
||||
float ao = texture(aoMap, fragUV).r;
|
||||
float ao = texture(aoMap, ApplyUv(4, fragUV)).r;
|
||||
vec3 diffuse = kD_amb * irradiance * albedo * ao;
|
||||
|
||||
vec3 R = reflect(-V, N);
|
||||
@@ -229,5 +259,7 @@ void main() {
|
||||
// alpha = albedo 贴图覆盖率 × baseColorFactor.a(glTF alphaMode=BLEND
|
||||
// 混合源);不透明资产两者恒 1 行为不变。预乘在 tonemap 段完成。
|
||||
outColor = vec4(ambient + Lo + emissive, albedoTex.a * matParams.baseColor.a);
|
||||
// ==== DEBUG B1: 纯 albedo 直出(验证贴图绑定/UV 采样)====
|
||||
// outColor = vec4(albedo, 1.0);
|
||||
outVelocity = fragVelocity;
|
||||
}
|
||||
|
||||
@@ -44,8 +44,20 @@ layout(set = 1, binding = 5) uniform sampler2D roughnessMap; // 独立粗糙度
|
||||
layout(set = 1, binding = 6) uniform MaterialParams {
|
||||
vec4 factors; // x = metallic 因子,y = roughness 因子,z = 独立粗糙度图开关,w = MASK 镂空开关
|
||||
vec4 baseColor; // glTF baseColorFactor:rgb 调色 albedo,a = BLEND 透明度
|
||||
vec4 surface; // x = ior,y = transmission(KHR_materials_transmission),z = 自发光强度,w = 体积厚度
|
||||
vec4 misc; // x = unlit(透射材质不会是 unlit,仅为与 simple_shader 布局一致)
|
||||
vec4 uvA[6]; // 每槽 UV 线性变换(KHR_texture_transform,v-up 空间)(a11,a12,a21,a22)
|
||||
vec4 uvT[6]; // 每槽 UV 平移 (tx, ty, 0, 0)
|
||||
} matParams;
|
||||
|
||||
// 每槽 UV 变换:uv' = A·uv + t(与 simple_shader.frag 同款,6 = kSlotCount)
|
||||
vec2 ApplyUv(int slot, vec2 uv) {
|
||||
vec4 a = matParams.uvA[slot];
|
||||
vec4 t = matParams.uvT[slot];
|
||||
return vec2(a.x * uv.x + a.y * uv.y + t.x,
|
||||
a.z * uv.x + a.w * uv.y + t.y);
|
||||
}
|
||||
|
||||
// CSM 阴影(sampler2DShadow:单次采样 = 硬件 2×2 深度比较 PCF)
|
||||
layout(set = 2, binding = 0) uniform sampler2DShadow shadowMap0; // CSM 级联 0(最近)
|
||||
layout(set = 2, binding = 1) uniform sampler2DShadow shadowMap1; // 级联 1
|
||||
@@ -53,6 +65,7 @@ layout(set = 2, binding = 2) uniform sampler2DShadow shadowMap2; // 级联 2
|
||||
layout(set = 2, binding = 3) uniform samplerCube irradianceMap; // 漫反射环境光(IBL 阶段②)
|
||||
layout(set = 2, binding = 4) uniform samplerCube prefilteredMap; // 镜面环境(mip 分 roughness,阶段③)
|
||||
layout(set = 2, binding = 5) uniform sampler2D brdfLut; // BRDF 积分表 (NdotV, roughness) → (scale, bias)
|
||||
layout(set = 2, binding = 6) uniform sampler2D sceneColor; // 不透明段快照(透射折射采样源)
|
||||
|
||||
// 第 0 盏方向光(KeyLight)的 CSM 阴影因子。
|
||||
// 级联按视图深度选择;3×3 纹素偏移采样(每 tap 硬件 2×2 PCF,
|
||||
@@ -136,21 +149,35 @@ vec3 FresnelSchlickRoughness(float cosTheta, vec3 F0, float roughness) {
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec4 albedoTex = texture(albedoMap, fragUV);
|
||||
vec4 albedoTex = texture(albedoMap, ApplyUv(0, fragUV));
|
||||
vec3 albedo = albedoTex.rgb * matParams.baseColor.rgb;
|
||||
vec4 mr = texture(metallicRoughnessMap, fragUV);
|
||||
vec4 mr = texture(metallicRoughnessMap, ApplyUv(2, fragUV));
|
||||
// 粗糙度源按 factors.z 切换:glTF 合图工作流读 MR.g;
|
||||
// FBX/OBJ specular 工作流(z=1)换用独立粗糙度图 R 通道
|
||||
float roughness = clamp(mix(mr.g, texture(roughnessMap, fragUV).r,
|
||||
float roughness = clamp(mix(mr.g, texture(roughnessMap, ApplyUv(5, fragUV)).r,
|
||||
matParams.factors.z) * matParams.factors.y,
|
||||
0.045, 1.0);
|
||||
float metallic = clamp(mr.b * matParams.factors.x, 0.0, 1.0);
|
||||
vec3 emissive = texture(emissiveMap, fragUV).rgb;
|
||||
// 自发光强度(KHR_materials_emissive_strength,缺省 1)
|
||||
vec3 emissive = texture(emissiveMap, ApplyUv(3, fragUV)).rgb * matParams.surface.z;
|
||||
|
||||
vec3 N = MappedNormal(normalize(fragNormal), fragTangent, fragUV);
|
||||
// glTF 表面扩展:透射率(KHR_materials_transmission)/
|
||||
// 折射率(ior,缺省 1.5)/ 体积厚度(KHR_materials_volume)
|
||||
const float transmission = clamp(matParams.surface.y, 0.0, 1.0);
|
||||
const float ior = max(matParams.surface.x, 1.0);
|
||||
const float thickness = max(matParams.surface.w, 0.0);
|
||||
|
||||
vec3 N = MappedNormal(normalize(fragNormal), fragTangent, ApplyUv(1, fragUV));
|
||||
vec3 V = normalize(ubo.cameraPos.xyz - fragWorldPos);
|
||||
// 双面法线:cull 关闭,背面片元(玻璃壳内壁)几何法线背向相机,
|
||||
// 光照/折射方向全反——按外表面等价翻转到视侧
|
||||
if (dot(N, V) < 0.0) N = -N;
|
||||
float NdotV = max(dot(N, V), 1e-4);
|
||||
vec3 F0 = mix(vec3(0.04), albedo, metallic); // 电介质基准反射率 4%
|
||||
// F0 按折射率换算(KHR_materials_ior):((ior-1)/(ior+1))²;
|
||||
// glTF 缺省 ior=1.5 → 0.04,与旧固定值一致
|
||||
vec3 F0 = mix(vec3((ior - 1.0) / (ior + 1.0)
|
||||
* (ior - 1.0) / (ior + 1.0)),
|
||||
albedo, metallic); // 电介质 F0 / 金属 albedo
|
||||
|
||||
vec3 Lo = vec3(0.0);
|
||||
float shade0 = 1.0f; // 第 0 盏光的阴影因子(环境光同步衰减用)
|
||||
@@ -185,7 +212,10 @@ void main() {
|
||||
vec3 F = FresnelSchlick(NdotH2, F0);
|
||||
|
||||
vec3 kS = F;
|
||||
vec3 kD = (1.0 - kS) * (1.0 - metallic); // 金属不参与漫反射
|
||||
// 漫反射能量让位给透射:真玻璃无漫反射(入射光 = 反射 F +
|
||||
// 透射 1-F),transmission=1 时 kD→0 = 纯镜面电介质;透射光
|
||||
// 由场景色副本折射采样补回(见下方 transmitted)
|
||||
vec3 kD = (1.0 - kS) * (1.0 - metallic) * (1.0 - transmission);
|
||||
vec3 specular = (D * G * F) /
|
||||
max(4.0 * NdotV * NdotL, 1e-4);
|
||||
|
||||
@@ -207,8 +237,9 @@ void main() {
|
||||
// 镜面:R 采样 pre-filtered cube(mip = roughness 分级)
|
||||
// × BRDF LUT 查表得 F 缩放/偏置
|
||||
// ambientColor = 用户可调强度/色调乘子
|
||||
// 漫反射侧同步乘 (1-transmission)(同直接光能量重分配)
|
||||
vec3 F_amb = FresnelSchlickRoughness(NdotV, F0, roughness);
|
||||
vec3 kD_amb = (1.0 - F_amb) * (1.0 - metallic);
|
||||
vec3 kD_amb = (1.0 - F_amb) * (1.0 - metallic) * (1.0 - transmission);
|
||||
|
||||
vec3 irradiance = texture(irradianceMap, N).rgb;
|
||||
float ao = texture(aoMap, fragUV).r;
|
||||
@@ -224,10 +255,34 @@ void main() {
|
||||
vec3 ambient = (diffuse + specular) * ubo.ambientColor.rgb
|
||||
* mix(0.4, 1.0, shade0); // 阴影区环境同步衰减(简化遮蔽)
|
||||
|
||||
// alpha = albedo 贴图覆盖率 × baseColorFactor.a(glTF alphaMode=BLEND
|
||||
// 混合源;玻璃类无 baseColor 贴图材质的透明度全靠 factor.a)。
|
||||
// ---- 透射(KHR_materials_transmission):屏幕空间折射 ----
|
||||
// 采样源 = 不透明段快照(天空盒 + 不透明几何,Forward 后
|
||||
// ForwardTrans 前拷贝)。玻璃互相不可见(副本不含透明队列),
|
||||
// 与 UE 屏幕空间透射同一限制。
|
||||
vec3 transmitted = vec3(0.0);
|
||||
if (transmission > 0.0) {
|
||||
// 进入介质 eta = 1/ior;全内反射(TIR)退化为反射方向
|
||||
vec3 refrDir = refract(-V, N, 1.0 / ior);
|
||||
if (dot(refrDir, refrDir) < 1e-6) refrDir = reflect(-V, N);
|
||||
// 世界空间步进 thickness 投回屏幕取折射像素(thickness=0 =
|
||||
// 直透同位采样,无弯曲);projNoJitter 保证折射样本位置
|
||||
// 无 TAA 抖动(时域稳定收敛)
|
||||
vec4 clip = ubo.projNoJitter * ubo.view *
|
||||
vec4(fragWorldPos + refrDir * thickness, 1.0);
|
||||
vec2 refrUV = (clip.xy / clip.w) * 0.5 + 0.5;
|
||||
refrUV = clamp(refrUV, vec2(0.0), vec2(1.0));
|
||||
vec3 scene = texture(sceneColor, refrUV).rgb;
|
||||
// albedo 染色 = 有色玻璃近似(Beer-Lambert 体积吸收后续演进)
|
||||
transmitted = scene * albedo * transmission;
|
||||
}
|
||||
|
||||
// alpha = max(覆盖率, transmission):transmission 已烘进 rgb(透射
|
||||
// 背景是颜色的一部分),t≈1 的玻璃对混合方程是完全替换像素
|
||||
//(src*1 + dst*0)——不再有"白 albedo×a 混合"的奶白罩。
|
||||
// 非 transmission 的 BLEND 材质(软粒子/光效)行为不变。
|
||||
// 预乘在 tonemap 段完成。
|
||||
outColor = vec4(ambient + Lo + emissive, albedoTex.a * matParams.baseColor.a);
|
||||
outColor = vec4(ambient + Lo + transmitted + emissive,
|
||||
max(albedoTex.a * matParams.baseColor.a, transmission));
|
||||
outVelocity = fragVelocity;
|
||||
|
||||
// 深度语义:真实深度测试 + 真实写入(depthWrite 状态保持 TRUE——
|
||||
|
||||
Reference in New Issue
Block a user