feat: ImGui docking Editor + 显示链/帧同步/输入语义重构
- App/editor: Hierarchy/Inspector/Viewport/Stats 四面板 + DockBuilder 默认布局,main.cpp 瘦身为场景搭建与生命周期 - 显示链: Tonemap→PresentTarget 经 ImGui::Image 呈现(零色偏),backbuffer 仅承载 ImGui;Direct/Viewport 双显示模式 + 面板分辨率上报 - 帧首同步 BeginFrameSync: resize/vsync/尺寸/模式意图统一帧首落定(修描述符悬垂);修复 vsync 意图未落 swapchain 导致的每帧重建交换链 - 输入: Editor 每帧判定键盘/鼠标归属(打字让位、Viewport hover/focus 放行、Fullscreen 全放行) - 材质: roughness 独立槽位 + 非 glTF 金属/粗糙贴图映射;透明队列 depthWrite=FALSE + translucent 独立 shader + scratch 清帧 - FBX: 非蒙皮网格烘焙节点链全局变换(Z-up 旋转/单位换算落顶点) - Hierarchy 行 ImGui ID 以 UUID 作用域(重名不冲突、改名不丢选中)
This commit is contained in:
@@ -14,21 +14,36 @@ namespace Engine {
|
||||
|
||||
void Camera::Update(Window& window, ECS::TransformComponent& t, f32 deltaTime)
|
||||
{
|
||||
// 鼠标旋转:直接写实体旋转(yaw → rotation.y,pitch → rotation.x)
|
||||
if (m_MouseLookEnabled)
|
||||
// 鼠标视角(编辑器习惯):按住右键捕获光标进入自由视角,
|
||||
// 松开释放光标(ImGui 面板可正常交互)。m_MouseLookEnabled 总开关
|
||||
const bool wantLook = m_MouseLookEnabled &&
|
||||
window.IsMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT);
|
||||
if (wantLook)
|
||||
{
|
||||
if (!window.IsCursorCaptured())
|
||||
{
|
||||
window.SetCursorCaptured(true);
|
||||
}
|
||||
|
||||
f32 mouseDX = 0, mouseDY = 0;
|
||||
window.GetMouseDelta(mouseDX, mouseDY);
|
||||
|
||||
if (mouseDX != 0 || mouseDY != 0)
|
||||
{
|
||||
glm::vec3 rot = t.GetRotation();
|
||||
rot.y += mouseDX * m_MouseSensitivity;
|
||||
// 右手系绕 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);
|
||||
t.SetRotation(rot);
|
||||
}
|
||||
}
|
||||
else if (window.IsCursorCaptured())
|
||||
{
|
||||
window.SetCursorCaptured(false);
|
||||
}
|
||||
|
||||
// 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X,
|
||||
// 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机)
|
||||
|
||||
+293
-38
@@ -22,6 +22,19 @@
|
||||
|
||||
namespace Engine {
|
||||
|
||||
namespace {
|
||||
// assimp 行主矩阵 → glm 列主(转置搬运;骨骼 node TRS / invBind 共用)
|
||||
glm::mat4 ToGlm(const aiMatrix4x4& m)
|
||||
{
|
||||
glm::mat4 out;
|
||||
out[0][0] = m.a1; out[1][0] = m.a2; out[2][0] = m.a3; out[3][0] = m.a4;
|
||||
out[0][1] = m.b1; out[1][1] = m.b2; out[2][1] = m.b3; out[3][1] = m.b4;
|
||||
out[0][2] = m.c1; out[1][2] = m.c2; out[2][2] = m.c3; out[3][2] = m.c4;
|
||||
out[0][3] = m.d1; out[1][3] = m.d2; out[2][3] = m.d3; out[3][3] = m.d4;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
@@ -76,7 +89,10 @@ void Model::InitDefaultTextures()
|
||||
m_DefaultTextures[Material::Emissive] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0);
|
||||
m_DefaultTextures[Material::Ao] =
|
||||
Texture::CreateSolidColor(m_Device, m_Allocator, 255, 255, 255, false);
|
||||
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);
|
||||
}
|
||||
|
||||
Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
@@ -89,12 +105,18 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
, m_DescriptorManager(descriptorManager)
|
||||
, m_TextureLayout(textureLayout)
|
||||
{
|
||||
// 纯色贴图(albedo 兜底;alpha 非全 1 时为半透明材质)
|
||||
// 纯色贴图直供 albedo 槽(alpha 非全 1 时为半透明材质);
|
||||
// 生命周期挂进纹理缓存(键含 rgba 值,同色共享、异色互不冲突)
|
||||
InitDefaultTextures();
|
||||
auto tex = (rgba[3] == 255)
|
||||
Scope<Texture> tex = (rgba[3] == 255)
|
||||
? Texture::CreateSolidColor(device, allocator, rgba[0], rgba[1], rgba[2])
|
||||
: Texture::CreateSolidColorRGBA(device, allocator,
|
||||
rgba[0], rgba[1], rgba[2], rgba[3]);
|
||||
Texture* albedoOverride = tex.get();
|
||||
const std::string texKey = "proc:" + std::to_string(rgba[0]) + ',' +
|
||||
std::to_string(rgba[1]) + ',' + std::to_string(rgba[2]) + ',' +
|
||||
std::to_string(rgba[3]);
|
||||
m_TextureCache.emplace(texKey, std::move(tex));
|
||||
|
||||
// XZ 平面,法线 +Y,从上往下看 CCW 绕序
|
||||
const f32 h = halfSize;
|
||||
@@ -107,9 +129,13 @@ Model::Model(Device& device, MemoryAllocator& allocator,
|
||||
const std::vector<u32> indices = {0, 1, 2, 0, 2, 3};
|
||||
const std::vector<std::vector<u32>> lodIndices = {indices};
|
||||
|
||||
// 材质/纹理生命周期随模型(缓存持有)
|
||||
// 材质/纹理生命周期随模型(缓存持有);颜色纹理必须显式覆盖 albedo
|
||||
// 槽——缺了会折到默认白纹理(白板叠白地 → 平面“消失”的教训)
|
||||
std::string paths[Material::kSlotCount] = {"", "", "", ""};
|
||||
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, transparent);
|
||||
std::array<Texture*, Material::kSlotCount> overrides{};
|
||||
overrides[Material::Albedo] = albedoOverride;
|
||||
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, transparent,
|
||||
overrides);
|
||||
|
||||
m_Meshes.push_back(CreateScope<Mesh>(
|
||||
m_Device, m_Allocator, vertices, lodIndices, mat));
|
||||
@@ -164,20 +190,177 @@ ParsedModel Model::ParseFromDisk(const std::string& path)
|
||||
(out.isGltf ? 0 : aiProcess_FlipUVs) |
|
||||
aiProcess_GenSmoothNormals |
|
||||
aiProcess_CalcTangentSpace | // 法线贴图 TBN
|
||||
aiProcess_JoinIdenticalVertices);
|
||||
aiProcess_JoinIdenticalVertices |
|
||||
aiProcess_LimitBoneWeights); // 每顶点限 4 权重(= 顶点格式 boneIds 容量)
|
||||
|
||||
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
|
||||
if (!scene->mNumMeshes) {
|
||||
ENGINE_WARN("Assimp warn: model dose not have mesh assets\nmodel has following asset:");
|
||||
|
||||
ENGINE_WARN("\tMeshes Count: {}", scene->mNumMeshes);
|
||||
ENGINE_WARN("\tAnimations Count: {}", scene->mNumAnimations);
|
||||
ENGINE_WARN("\tCameras Count: {}", scene->mNumCameras);
|
||||
ENGINE_WARN("\tLights Count: {}", scene->mNumLights);
|
||||
ENGINE_WARN("\tMaterials Count: {}", scene->mNumMaterials);
|
||||
ENGINE_WARN("\tSkeletons Count: {}", scene->mNumSkeletons);
|
||||
ENGINE_WARN("\tTextures Count: {}", scene->mNumTextures);
|
||||
|
||||
}
|
||||
ENGINE_ERROR("Assimp error: {}", importer.GetErrorString());
|
||||
return out; // meshes 空 = 无效
|
||||
}
|
||||
|
||||
// 骨骼动画三件套:node 表(global 累乘骨架)→ 动画轨道 → 骨骼注册
|
||||
//(DFS 序;ProcessNode 的 mesh 遍历序与其独立,nodeIdx 靠 name 查表)
|
||||
FlattenNodes(scene->mRootNode, -1, out.nodes);
|
||||
ParseAnimations(scene, out.nodes, out.clips);
|
||||
|
||||
const std::filesystem::path modelDir =
|
||||
std::filesystem::path(path).parent_path();
|
||||
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf);
|
||||
ENGINE_INFO("Model parsed: {} ({} meshes)", path, out.meshes.size());
|
||||
// 骨骼注册表(boneName → palette slot):多 mesh 共享骨架去重
|
||||
std::unordered_map<std::string, u32> boneSlots;
|
||||
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf,
|
||||
out.bones, boneSlots);
|
||||
|
||||
// 骨骼 nodeIdx 回填(注册发生在各 mesh 的 ProcessMesh 内,
|
||||
// 这里按 node 表查 name 定位累乘路径)
|
||||
if (!out.bones.empty()) {
|
||||
std::unordered_map<std::string, u32> nodeByName;
|
||||
nodeByName.reserve(out.nodes.size());
|
||||
for (u32 i = 0; i < out.nodes.size(); ++i) {
|
||||
nodeByName.emplace(out.nodes[i].name, i);
|
||||
}
|
||||
for (auto& bone : out.bones) {
|
||||
if (auto it = nodeByName.find(bone.name); it != nodeByName.end()) {
|
||||
bone.nodeIdx = it->second;
|
||||
} else {
|
||||
ENGINE_WARN("Bone '{}' has no matching node, palette entry static",
|
||||
bone.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ENGINE_INFO("Model parsed: {} ({} meshes, {} bones, {} anims)",
|
||||
path, out.meshes.size(), out.bones.size(), out.clips.size());
|
||||
return out;
|
||||
}
|
||||
|
||||
u32 Model::FlattenNodes(const aiNode* node, i32 parent,
|
||||
std::vector<ModelNode>& out)
|
||||
{
|
||||
const u32 idx = static_cast<u32>(out.size());
|
||||
ModelNode n;
|
||||
n.name = node->mName.C_Str();
|
||||
n.parent = parent;
|
||||
// 完整 mTransformation:FBX 的 PreRotation/单位换算都折在这里,
|
||||
// 骨骼 global 累乘必须用全量矩阵,不能拆 TRS 重建
|
||||
n.localTransform = ToGlm(node->mTransformation);
|
||||
out.push_back(std::move(n));
|
||||
for (u32 i = 0; i < node->mNumChildren; ++i) {
|
||||
FlattenNodes(node->mChildren[i], static_cast<i32>(idx), out);
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
void Model::ParseAnimations(const aiScene* scene,
|
||||
const std::vector<ModelNode>& nodes,
|
||||
std::vector<AnimationClip>& out)
|
||||
{
|
||||
if (scene->mNumAnimations == 0) return;
|
||||
|
||||
// nodeName → node 表下标(channel 的目标必须能在层级里累乘)
|
||||
std::unordered_map<std::string, u32> nodeByName;
|
||||
nodeByName.reserve(nodes.size());
|
||||
for (u32 i = 0; i < nodes.size(); ++i) {
|
||||
nodeByName.emplace(nodes[i].name, i);
|
||||
}
|
||||
|
||||
for (u32 a = 0; a < scene->mNumAnimations; ++a) {
|
||||
const aiAnimation* anim = scene->mAnimations[a];
|
||||
AnimationClip clip;
|
||||
clip.name = anim->mName.length > 0 ? anim->mName.C_Str()
|
||||
: ("clip_" + std::to_string(a));
|
||||
// tps=0 按 1(assimp 对部分导出器的惯例缺省)
|
||||
clip.tps = anim->mTicksPerSecond > 0.0
|
||||
? static_cast<f32>(anim->mTicksPerSecond) : 1.0f;
|
||||
clip.duration = static_cast<f32>(anim->mDuration) / clip.tps;
|
||||
|
||||
for (u32 c = 0; c < anim->mNumChannels; ++c) {
|
||||
const aiNodeAnim* ch = anim->mChannels[c];
|
||||
auto it = nodeByName.find(ch->mNodeName.C_Str());
|
||||
if (it == nodeByName.end()) {
|
||||
ENGINE_WARN("Anim '{}': channel node '{}' not in hierarchy, skipped",
|
||||
clip.name, ch->mNodeName.C_Str());
|
||||
continue;
|
||||
}
|
||||
AnimChannel chan;
|
||||
chan.nodeIdx = it->second;
|
||||
// 关键帧 time 统一换算成秒(运行时采样免除法)
|
||||
chan.positions.reserve(ch->mNumPositionKeys);
|
||||
for (u32 k = 0; k < ch->mNumPositionKeys; ++k) {
|
||||
const auto& key = ch->mPositionKeys[k];
|
||||
chan.positions.push_back({
|
||||
static_cast<f32>(key.mTime) / clip.tps,
|
||||
{key.mValue.x, key.mValue.y, key.mValue.z}});
|
||||
}
|
||||
chan.rotations.reserve(ch->mNumRotationKeys);
|
||||
for (u32 k = 0; k < ch->mNumRotationKeys; ++k) {
|
||||
const auto& key = ch->mRotationKeys[k];
|
||||
// aiQuaternion 存储序 (w,x,y,z) → glm 构造序一致
|
||||
chan.rotations.push_back({
|
||||
static_cast<f32>(key.mTime) / clip.tps,
|
||||
glm::quat(key.mValue.w, key.mValue.x,
|
||||
key.mValue.y, key.mValue.z)});
|
||||
}
|
||||
chan.scales.reserve(ch->mNumScalingKeys);
|
||||
for (u32 k = 0; k < ch->mNumScalingKeys; ++k) {
|
||||
const auto& key = ch->mScalingKeys[k];
|
||||
chan.scales.push_back({
|
||||
static_cast<f32>(key.mTime) / clip.tps,
|
||||
{key.mValue.x, key.mValue.y, key.mValue.z}});
|
||||
}
|
||||
clip.channels.push_back(std::move(chan));
|
||||
}
|
||||
out.push_back(std::move(clip));
|
||||
}
|
||||
}
|
||||
|
||||
void Model::CollectMeshBones(const aiMesh* mesh,
|
||||
std::vector<Vertex>& vertices,
|
||||
std::vector<ModelBone>& bones,
|
||||
std::unordered_map<std::string, u32>& boneSlots)
|
||||
{
|
||||
for (u32 b = 0; b < mesh->mNumBones; ++b) {
|
||||
const aiBone* bone = mesh->mBones[b];
|
||||
const char* name = bone->mName.C_Str();
|
||||
|
||||
u32 slot;
|
||||
if (auto it = boneSlots.find(name); it != boneSlots.end()) {
|
||||
slot = it->second; // 多 mesh 共享同一骨架(身体/附件分区)
|
||||
} else {
|
||||
slot = static_cast<u32>(bones.size());
|
||||
boneSlots.emplace(name, slot);
|
||||
// nodeIdx 由 ParseFromDisk 统一回填(node 表此时已就绪)
|
||||
bones.push_back({name, 0, ToGlm(bone->mOffsetMatrix)});
|
||||
}
|
||||
|
||||
// 顶点权重累加:LimitBoneWeights 保证每顶点 ≤4 根骨,逐槽找空位;
|
||||
// 4 槽满的理论残留权重直接忽略(量化误差级)
|
||||
for (u32 w = 0; w < bone->mNumWeights; ++w) {
|
||||
const aiVertexWeight& vw = bone->mWeights[w];
|
||||
if (vw.mVertexId >= vertices.size() || vw.mWeight <= 0.0f) continue;
|
||||
Vertex& v = vertices[vw.mVertexId];
|
||||
for (u32 k = 0; k < 4; ++k) {
|
||||
if (v.weights[k] == 0.0f) {
|
||||
v.boneIds[k] = slot;
|
||||
v.weights[k] = vw.mWeight;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptorManager,
|
||||
vk::DescriptorSetLayout textureLayout,
|
||||
@@ -192,6 +375,9 @@ Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
|
||||
descriptorManager, textureLayout));
|
||||
model->m_ModelPath = parsed.path;
|
||||
model->m_IsGltf = parsed.isGltf;
|
||||
model->m_Nodes = std::move(parsed.nodes);
|
||||
model->m_Bones = std::move(parsed.bones);
|
||||
model->m_Clips = std::move(parsed.clips);
|
||||
|
||||
// 材质缓存去重 + 纹理上传 + mesh 缓冲(全部 GPU,主线程)
|
||||
for (auto& pm : parsed.meshes) {
|
||||
@@ -214,6 +400,9 @@ void Model::LoadModel(const std::string& path)
|
||||
}
|
||||
m_ModelPath = parsed.path;
|
||||
m_IsGltf = parsed.isGltf;
|
||||
m_Nodes = std::move(parsed.nodes);
|
||||
m_Bones = std::move(parsed.bones);
|
||||
m_Clips = std::move(parsed.clips);
|
||||
for (auto& pm : parsed.meshes) {
|
||||
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
|
||||
m_Meshes.push_back(CreateScope<Mesh>(
|
||||
@@ -226,14 +415,24 @@ void Model::LoadModel(const std::string& path)
|
||||
|
||||
void Model::ProcessNode(const aiNode* node, const aiScene* scene,
|
||||
std::vector<ParsedMesh>& out,
|
||||
const std::filesystem::path& modelDir, bool isGltf)
|
||||
const std::filesystem::path& modelDir, bool isGltf,
|
||||
std::vector<ModelBone>& bones,
|
||||
std::unordered_map<std::string, u32>& boneSlots,
|
||||
const aiMatrix4x4& parentGlobal)
|
||||
{
|
||||
// 累乘节点链全局变换:Blender/Sketchfab 导出的 FBX(未勾 Apply
|
||||
// Transform)把物体旋转(含 Z-up→Y-up)与 cm→m 单位换算全折在
|
||||
// 节点变换上,忽略会让每个零件按原始局部空间乱画(朝向丢失、
|
||||
// 尺度差百倍,车都不成车)
|
||||
const aiMatrix4x4 global = parentGlobal * node->mTransformation;
|
||||
for (u32 i = 0; i < node->mNumMeshes; ++i) {
|
||||
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
|
||||
out.push_back(ProcessMesh(mesh, scene, modelDir, isGltf));
|
||||
out.push_back(ProcessMesh(mesh, scene, modelDir, isGltf,
|
||||
bones, boneSlots, global));
|
||||
}
|
||||
for (u32 i = 0; i < node->mNumChildren; ++i) {
|
||||
ProcessNode(node->mChildren[i], scene, out, modelDir, isGltf);
|
||||
ProcessNode(node->mChildren[i], scene, out, modelDir, isGltf,
|
||||
bones, boneSlots, global);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,21 +463,37 @@ Texture* Model::GetOrLoadTexture(const std::string& rawPath, bool srgb)
|
||||
|
||||
Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
|
||||
const MaterialParams& params,
|
||||
bool transparent)
|
||||
bool transparent,
|
||||
const std::array<Texture*, Material::kSlotCount>&
|
||||
slotOverrides)
|
||||
{
|
||||
const std::string key = Material::MakeCacheKey(paths, params, transparent);
|
||||
// 程序化覆盖纹理以指针地址入键:同模型内不同颜色的覆盖组合
|
||||
//(如两块玻璃板)各得一份材质,不与纯路径键的材质串缓存
|
||||
std::string key = Material::MakeCacheKey(paths, params, transparent);
|
||||
bool hasOverride = false;
|
||||
for (const Texture* t : slotOverrides) {
|
||||
if (t) { hasOverride = true; break; }
|
||||
}
|
||||
if (hasOverride) {
|
||||
key += "@ovr";
|
||||
for (const Texture* t : slotOverrides) {
|
||||
key += t ? ":" + std::to_string(reinterpret_cast<uintptr_t>(t))
|
||||
: ":0";
|
||||
}
|
||||
}
|
||||
|
||||
if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) {
|
||||
return it->second.get();
|
||||
}
|
||||
|
||||
// 每槽位解析纹理:空/缺失折到该槽位默认纹理
|
||||
// 每槽位解析纹理:程序化覆盖优先,路径次之,空/缺失折到默认纹理
|
||||
std::array<Texture*, Material::kSlotCount> textures{};
|
||||
// 槽位 sRGB 语义:颜色图 true,数据图 false
|
||||
const bool slotSrgb[Material::kSlotCount] = {true, false, false, true};
|
||||
|
||||
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
|
||||
Texture* tex = nullptr;
|
||||
if (!paths[slot].empty()) {
|
||||
Texture* tex = slotOverrides[slot];
|
||||
if (!tex && !paths[slot].empty()) {
|
||||
tex = GetOrLoadTexture(paths[slot], slotSrgb[slot]);
|
||||
}
|
||||
textures[slot] = tex ? tex : m_DefaultTextures[slot].get();
|
||||
@@ -294,29 +509,47 @@ Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCoun
|
||||
|
||||
ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
const std::filesystem::path& modelDir,
|
||||
bool isGltf)
|
||||
bool isGltf,
|
||||
std::vector<ModelBone>& bones,
|
||||
std::unordered_map<std::string, u32>& boneSlots,
|
||||
const aiMatrix4x4& globalTransform)
|
||||
{
|
||||
ParsedMesh out;
|
||||
std::vector<Vertex>& vertices = out.vertices;
|
||||
std::vector<u32> indices;
|
||||
|
||||
vertices.reserve(mesh->mNumVertices);
|
||||
// 有骨骼的 mesh:顶点权重先清零再由 CollectMeshBones 灌入——
|
||||
// 默认 w=(1,0,0,0) 的槽 0 若不清,palette[0] 的 identity 会以
|
||||
// 权重 1 混进蒙皮矩阵(顶点永远钉在原点)
|
||||
const bool hasBones = mesh->mNumBones > 0;
|
||||
|
||||
// 非蒙皮 mesh:烘焙节点链全局变换(旋转/单位换算折进顶点);
|
||||
// 蒙皮 mesh 保持 mesh 空间——palette × offsetMatrix 的蒙皮
|
||||
// 约定依赖原空间,烘焙会与动画路径打架
|
||||
const bool bakeTransform = !hasBones;
|
||||
const aiMatrix3x3 normalMat(globalTransform);
|
||||
|
||||
for (u32 i = 0; i < mesh->mNumVertices; ++i) {
|
||||
Vertex vertex{};
|
||||
|
||||
vertex.position = {
|
||||
mesh->mVertices[i].x,
|
||||
mesh->mVertices[i].y,
|
||||
mesh->mVertices[i].z
|
||||
};
|
||||
if (bakeTransform) {
|
||||
const aiVector3D p = globalTransform * mesh->mVertices[i];
|
||||
vertex.position = {p.x, p.y, p.z};
|
||||
} else {
|
||||
vertex.position = {
|
||||
mesh->mVertices[i].x,
|
||||
mesh->mVertices[i].y,
|
||||
mesh->mVertices[i].z
|
||||
};
|
||||
}
|
||||
|
||||
if (mesh->HasNormals()) {
|
||||
vertex.normal = {
|
||||
mesh->mNormals[i].x,
|
||||
mesh->mNormals[i].y,
|
||||
mesh->mNormals[i].z
|
||||
};
|
||||
const aiVector3D n = bakeTransform
|
||||
? normalMat * mesh->mNormals[i]
|
||||
: mesh->mNormals[i];
|
||||
const glm::vec3 nn{n.x, n.y, n.z};
|
||||
vertex.normal = bakeTransform ? glm::normalize(nn) : nn;
|
||||
}
|
||||
|
||||
if (mesh->mTextureCoords[0]) {
|
||||
@@ -328,16 +561,23 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
|
||||
// 切线(法线贴图 TBN;无切线数据置零,shader 侧退回几何法线)
|
||||
if (mesh->mTangents) {
|
||||
vertex.tangent = {
|
||||
mesh->mTangents[i].x,
|
||||
mesh->mTangents[i].y,
|
||||
mesh->mTangents[i].z
|
||||
};
|
||||
const aiVector3D t = bakeTransform
|
||||
? normalMat * mesh->mTangents[i]
|
||||
: mesh->mTangents[i];
|
||||
const glm::vec3 tt{t.x, t.y, t.z};
|
||||
vertex.tangent = bakeTransform ? glm::normalize(tt) : tt;
|
||||
}
|
||||
|
||||
if (hasBones) {
|
||||
vertex.weights = glm::vec4(0.0f);
|
||||
}
|
||||
|
||||
vertices.push_back(vertex);
|
||||
}
|
||||
|
||||
// 骨骼权重灌顶点(无骨骼 mesh 顶点保持默认 idx0/w1 → identity 直通)
|
||||
CollectMeshBones(mesh, vertices, bones, boneSlots);
|
||||
|
||||
for (u32 i = 0; i < mesh->mNumFaces; ++i) {
|
||||
aiFace face = mesh->mFaces[i];
|
||||
for (u32 j = 0; j < face.mNumIndices; ++j) {
|
||||
@@ -355,16 +595,19 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
// glTF2 贴图在 assimp 5.x 的落点:MR 合图 → METALNESS(15)/
|
||||
// DIFFUSE_ROUGHNESS(16)(同图双槽);baseColor → BASE_COLOR(12)。
|
||||
// OBJ:map_Ns → SHININESS(独立 roughness 图)、map_Ka 常绑 AO。
|
||||
// 先到先得,数组顺序即优先级
|
||||
// 先到先得,数组顺序即优先级。
|
||||
// 注意:FBX 独立 metalness/roughness 是两张图——metal 落 MR 槽
|
||||
//(灰度 R=G=B,shader 读 .b 当金属度恰好正确),roughness 落
|
||||
// 独立 Roughness 槽(factors.z=1 启用),不再挤占 MR 槽的 .g
|
||||
const std::pair<aiTextureType, u32> slots[] = {
|
||||
{aiTextureType_DIFFUSE, Material::Albedo},
|
||||
{aiTextureType_BASE_COLOR, Material::Albedo},
|
||||
{aiTextureType_NORMALS, Material::Normal},
|
||||
{aiTextureType_HEIGHT, Material::Normal}, // OBJ map_Bump 落点
|
||||
{aiTextureType_METALNESS, Material::MetallicRoughness},
|
||||
{aiTextureType_DIFFUSE_ROUGHNESS, Material::MetallicRoughness},
|
||||
{aiTextureType_DIFFUSE_ROUGHNESS, Material::Roughness},
|
||||
{aiTextureType_SHININESS, Material::Roughness}, // OBJ map_Ns
|
||||
{aiTextureType_UNKNOWN, Material::MetallicRoughness},
|
||||
{aiTextureType_SHININESS, Material::MetallicRoughness},
|
||||
{aiTextureType_AMBIENT_OCCLUSION, Material::Ao},
|
||||
{aiTextureType_LIGHTMAP, Material::Ao},
|
||||
{aiTextureType_AMBIENT, Material::Ao},
|
||||
@@ -385,7 +628,8 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
|
||||
// 材质加载可见性:贴图槽缺失/路径解析错误会静默退化成默认
|
||||
// 纹理(金属全没 → 塑料感),这里留下诊断锚点
|
||||
ENGINE_INFO("Material '{}': albedo={} normal={} MR={} emissive={} ao={}",
|
||||
ENGINE_INFO("Material '{}': albedo={} normal={} MR={} roughness={} "
|
||||
"emissive={} ao={}",
|
||||
material->GetName().C_Str(),
|
||||
texPaths[Material::Albedo].empty() ? "<none>"
|
||||
: texPaths[Material::Albedo],
|
||||
@@ -393,6 +637,8 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
: texPaths[Material::Normal],
|
||||
texPaths[Material::MetallicRoughness].empty() ? "<none>"
|
||||
: texPaths[Material::MetallicRoughness],
|
||||
texPaths[Material::Roughness].empty() ? "<none>"
|
||||
: texPaths[Material::Roughness],
|
||||
texPaths[Material::Emissive].empty() ? "<none>"
|
||||
: texPaths[Material::Emissive],
|
||||
texPaths[Material::Ao].empty() ? "<none>"
|
||||
@@ -419,15 +665,24 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
|
||||
out.transparent = (alphaMode == aiString("BLEND"));
|
||||
}
|
||||
} else {
|
||||
// FBX/OBJ 独立贴图工作流
|
||||
const bool hasSepRough = !texPaths[Material::Roughness].empty();
|
||||
f32 roughnessFactor = 1.0f;
|
||||
if (texPaths[Material::MetallicRoughness].empty()) {
|
||||
if (!hasSepRough && texPaths[Material::MetallicRoughness].empty()) {
|
||||
// 无粗糙度贴图 → Blinn-Phong Ns 换算 R=sqrt(2/(Ns+2)),
|
||||
// 补偿默认 MR 贴图 G=0.5 → 因子 = 2R
|
||||
ai_real shininess = 10.0f;
|
||||
material->Get(AI_MATKEY_SHININESS, shininess);
|
||||
const f32 rough =
|
||||
std::sqrt(2.0f / (static_cast<f32>(shininess) + 2.0f));
|
||||
roughnessFactor = 2.0f * glm::clamp(rough, 0.05f, 1.0f);
|
||||
}
|
||||
params.factors = glm::vec4(0.0f, roughnessFactor, 0.0f, 0.0f);
|
||||
// 金属度图落 MR 槽(灰度 .b 即金属度语义):
|
||||
// 有图 metallic 因子 = 1,无图 = 0(OBJ specular 工作流无金属概念)
|
||||
const f32 metallicFactor =
|
||||
texPaths[Material::MetallicRoughness].empty() ? 0.0f : 1.0f;
|
||||
params.factors = glm::vec4(metallicFactor, roughnessFactor,
|
||||
hasSepRough ? 1.0f : 0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/9/23.
|
||||
//
|
||||
// ForwardPass:相机视角渲染到 HDR 颜色图(线性光照值)。
|
||||
// 显示变换不在本 pass——HDRColor 经 Tonemap 归一后才写 Backbuffer
|
||||
// (对齐 UE/Unity:SceneColor(HDR) → tonemapper → UI)。
|
||||
//
|
||||
// 透明管线对齐 UE/Unity 标准语义:
|
||||
// - 不透明("Forward"):深度测试 + 写,无混合直写
|
||||
// - 透明("ForwardTrans"):独立渲染实例,深度测试(对不透明)+
|
||||
// 深度写入关闭,HDR/velocity/depth 全部 Load 承接不透明段结果,
|
||||
// 预乘 alpha 混合(线性空间 over),远 → 近排序(SceneRenderer
|
||||
// 侧按 batch 平均距离排)
|
||||
//
|
||||
// NVIDIA shader-object 实测注意(见 .trae/documents 性能档案):
|
||||
// 早期实验称 depthWriteEnable=FALSE 有灾难病态(~5fps),但该实验
|
||||
// 是在 frag 仍写 gl_FragDepth 的时期做的(写 gl_FragDepth 会禁用
|
||||
// early-Z,归因很可能在它而非 FALSE 本身)。gl_FragDepth 已删除,
|
||||
// 透明段现为 depthWrite=FALSE;若病态复现,先排查二者再下结论。
|
||||
// 另:勿用 gl_FragDepth=far 模拟"只测不写"(eLess 下片元自身
|
||||
// 永远测试失败,透明队列全灭)。
|
||||
//
|
||||
|
||||
#include <Engine/Renderer/Passes/ForwardPass.h>
|
||||
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
||||
#include <Engine/Renderer/Vertex.h>
|
||||
#include <Engine/Renderer/Mesh.h>
|
||||
#include <Engine/Renderer/Material.h>
|
||||
@@ -12,8 +32,90 @@
|
||||
|
||||
namespace Engine::Passes
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// 直写方程:(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;
|
||||
}
|
||||
|
||||
constexpr vk::ColorComponentFlags kColorWriteMask =
|
||||
vk::ColorComponentFlagBits::eR |
|
||||
vk::ColorComponentFlagBits::eG |
|
||||
vk::ColorComponentFlagBits::eB |
|
||||
vk::ColorComponentFlagBits::eA;
|
||||
} // namespace
|
||||
|
||||
// 两段渲染共享的命令录制:shader/描述符绑定 + 几何/光栅/视口状态。
|
||||
// translucent=true 时绑定真实深度写入的透明 frag 变体。
|
||||
// 深度写与混合状态由各渲染实例作为初始态设置(见文件头注释)。
|
||||
void ForwardPass::RecordSharedState(const RG::RenderGraphContext& ctx,
|
||||
vk::CommandBuffer cmd,
|
||||
bool translucent) const
|
||||
{
|
||||
// 绑定 shader object
|
||||
std::array<vk::ShaderStageFlagBits, 2> stages = {
|
||||
vk::ShaderStageFlagBits::eVertex,
|
||||
vk::ShaderStageFlagBits::eFragment
|
||||
};
|
||||
std::array<vk::ShaderEXT, 2> shaders = {
|
||||
m_Gpu.forwardVert,
|
||||
translucent ? m_Gpu.translucentFrag : m_Gpu.forwardFrag};
|
||||
cmd.bindShadersEXT(stages, shaders);
|
||||
|
||||
// Set 0:本帧槽位的 UBO;Set 2:本帧槽位的阴影图
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 0,
|
||||
m_Gpu.uboSets[ctx.frame.frameSlot], {});
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 2,
|
||||
m_Gpu.shadowSets[ctx.frame.frameSlot], {});
|
||||
|
||||
cmd.setRasterizerDiscardEnable(VK_FALSE);
|
||||
|
||||
cmd.setDepthTestEnable(VK_TRUE);
|
||||
cmd.setDepthCompareOp(vk::CompareOp::eLess);
|
||||
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.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList);
|
||||
cmd.setPrimitiveRestartEnable(VK_FALSE);
|
||||
|
||||
auto binding = Vertex::GetBindingDescription();
|
||||
auto attributes = Vertex::GetAttributeDescriptions();
|
||||
cmd.setVertexInputEXT(binding, attributes);
|
||||
|
||||
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
|
||||
vk::SampleMask sampleMask = 0xFFFFFFFF;
|
||||
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
|
||||
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
|
||||
cmd.setAlphaToOneEnableEXT(VK_FALSE);
|
||||
|
||||
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);
|
||||
|
||||
vk::Rect2D scissor({0, 0}, ctx.extent);
|
||||
cmd.setScissorWithCount(scissor);
|
||||
}
|
||||
|
||||
void ForwardPass::Register(RG::RenderGraph& rg)
|
||||
{
|
||||
// ---- 不透明段:深度测试 + 写,无混合直写 ----
|
||||
rg.AddPass("Forward", [this](RG::RenderGraphContext& ctx) {
|
||||
const vk::CommandBuffer cmd = ctx.cmd;
|
||||
|
||||
@@ -42,6 +144,96 @@ namespace Engine::Passes
|
||||
depthAttachment.setImageView(ctx.View(m_Depth))
|
||||
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eClear)
|
||||
// 透明段要 Load 本段写出的深度做测试,必须 store
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(vk::ClearValue(
|
||||
vk::ClearDepthStencilValue(1.0f, 0)));
|
||||
|
||||
std::array<vk::RenderingAttachmentInfo, 2> colorArray = {
|
||||
colorAttachments[0], colorAttachments[1]};
|
||||
|
||||
vk::RenderingInfo renderingInfo;
|
||||
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
|
||||
.setLayerCount(1)
|
||||
.setColorAttachments(colorArray)
|
||||
.setPDepthAttachment(&depthAttachment);
|
||||
|
||||
cmd.beginRendering(renderingInfo);
|
||||
RecordSharedState(ctx, cmd, false);
|
||||
cmd.setDepthWriteEnable(VK_TRUE);
|
||||
|
||||
// 两个颜色附件(HDR + velocity)都是无混合直写
|
||||
cmd.setColorBlendEnableEXT(0,
|
||||
std::array<vk::Bool32, 2>{VK_FALSE, VK_FALSE});
|
||||
const vk::ColorBlendEquationEXT direct = MakeDirectEquation();
|
||||
cmd.setColorBlendEquationEXT(0,
|
||||
std::array<vk::ColorBlendEquationEXT, 2>{direct, direct});
|
||||
cmd.setColorWriteMaskEXT(0,
|
||||
std::array<vk::ColorComponentFlags, 2>{kColorWriteMask, kColorWriteMask});
|
||||
|
||||
// 调试视图 Transparent:只看透明队列——跳过不透明绘制,
|
||||
// HDR 颜色 = 清屏色 + 透明段内容(Skybox 同步跳过背景)
|
||||
if (m_Frame.debugView != Passes::kDebugTransparent) {
|
||||
for (const auto& batch : m_Frame.mainView.batches) {
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 1,
|
||||
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
|
||||
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
|
||||
}
|
||||
}
|
||||
|
||||
cmd.endRendering();
|
||||
})
|
||||
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
|
||||
.Write(m_Velocity, RG::ResourceUse::ColorAttachment)
|
||||
.Write(m_Depth, RG::ResourceUse::DepthAttachment)
|
||||
.Read(m_ShadowMaps[0], RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
.Read(m_ShadowMaps[1], RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
.Read(m_ShadowMaps[2], RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment);
|
||||
|
||||
// ---- 透明段:独立渲染实例,UE/Unity 标准语义 ----
|
||||
// 深度测试对不透明深度(eLess)+ 深度写入关闭——玻璃之间
|
||||
// 互相不遮挡(谁先画都留在画面上,先后只影响 over 混合的
|
||||
// 颜色数学),交叉半透明不再整块消失。
|
||||
// 对不透明深度的 RAW 由 depth 的 Write use 链保证排在
|
||||
// 不透明段之后(WAW 边)。
|
||||
// alpha 语义:frag 输出未预乘线性色 + 覆盖率 alpha,
|
||||
// 预乘在 tonemap 段(rgb * a)完成 —— hard constraint。
|
||||
rg.AddPass("ForwardTrans", [this](RG::RenderGraphContext& ctx) {
|
||||
if (m_Frame.translucentView.batches.empty()) return;
|
||||
|
||||
const vk::CommandBuffer cmd = ctx.cmd;
|
||||
|
||||
vk::ClearValue clearValue;
|
||||
clearValue.color = vk::ClearColorValue(
|
||||
std::array<float, 4>{0.1f, 0.1f, 0.1f, 0.0f});
|
||||
|
||||
vk::RenderingAttachmentInfo colorAttachments[2];
|
||||
colorAttachments[0].setImageView(ctx.View(m_HDRColor))
|
||||
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(clearValue);
|
||||
|
||||
// velocity:Load 不透明段写出的运动向量,玻璃只覆盖自己的像素
|
||||
vk::ClearValue velocityClear;
|
||||
velocityClear.color = vk::ClearColorValue(std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
|
||||
colorAttachments[1].setImageView(ctx.View(m_Velocity))
|
||||
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(velocityClear);
|
||||
|
||||
vk::RenderingAttachmentInfo depthAttachment;
|
||||
depthAttachment.setImageView(ctx.View(m_Depth))
|
||||
// 深度只读测试源:Load 承接不透明段深度,
|
||||
// 透明段 depthWrite=FALSE 不产生写入,
|
||||
// storeOp DontCare(下帧 entry 重新 Clear)
|
||||
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eDontCare)
|
||||
.setClearValue(vk::ClearValue(
|
||||
vk::ClearDepthStencilValue(1.0f, 0)));
|
||||
@@ -56,124 +248,49 @@ namespace Engine::Passes
|
||||
.setPDepthAttachment(&depthAttachment);
|
||||
|
||||
cmd.beginRendering(renderingInfo);
|
||||
RecordSharedState(ctx, cmd, true);
|
||||
// UE/Unity 语义:透明段只测不写深度。写深度会让"先画的玻璃"
|
||||
// 剔掉后画但实际在它前面的玻璃(交叉半透明整块消失)。
|
||||
// 早期 depthWrite=FALSE 引发 NVIDIA ~5fps 病态的实验是在
|
||||
// frag 仍写 gl_FragDepth 的时期做的(禁 early-Z),归因存疑;
|
||||
// gl_FragDepth 已删除,FALSE 若复现病态再另行隔离。
|
||||
cmd.setDepthWriteEnable(VK_FALSE);
|
||||
|
||||
// 绑定 shader object
|
||||
std::array<vk::ShaderStageFlagBits, 2> stages = {
|
||||
vk::ShaderStageFlagBits::eVertex,
|
||||
vk::ShaderStageFlagBits::eFragment
|
||||
};
|
||||
std::array<vk::ShaderEXT, 2> shaders = {m_Gpu.forwardVert, m_Gpu.forwardFrag};
|
||||
cmd.bindShadersEXT(stages, shaders);
|
||||
vk::ColorBlendEquationEXT premul;
|
||||
premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
|
||||
.setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
|
||||
.setColorBlendOp(vk::BlendOp::eAdd)
|
||||
// alpha 穿透:玻璃只做场景内 rgb over(透出背景色),
|
||||
// 不修改像素覆盖率——HDR 的 a 是"窗口透过率"(tonemap
|
||||
// rgb *= a 预乘 + 透明窗口桌面合成),归背景专属。若让
|
||||
// 玻璃 over 混入 a(旧写法),a<1 背景上的玻璃会被拖进
|
||||
// 桌面混色链路 → 预乘拉黑
|
||||
.setSrcAlphaBlendFactor(vk::BlendFactor::eZero)
|
||||
.setDstAlphaBlendFactor(vk::BlendFactor::eOne)
|
||||
.setAlphaBlendOp(vk::BlendOp::eAdd);
|
||||
|
||||
// Set 0:本帧槽位的 UBO;Set 2:本帧槽位的阴影图
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 0,
|
||||
m_Gpu.uboSets[ctx.frame.frameSlot], {});
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 2,
|
||||
m_Gpu.shadowSets[ctx.frame.frameSlot], {});
|
||||
|
||||
// ---- 动态状态 ----
|
||||
cmd.setRasterizerDiscardEnable(VK_FALSE);
|
||||
|
||||
cmd.setDepthTestEnable(VK_TRUE);
|
||||
cmd.setDepthWriteEnable(VK_TRUE);
|
||||
cmd.setDepthCompareOp(vk::CompareOp::eLess);
|
||||
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.setPrimitiveTopology(vk::PrimitiveTopology::eTriangleList);
|
||||
cmd.setPrimitiveRestartEnable(VK_FALSE);
|
||||
|
||||
auto binding = Vertex::GetBindingDescription();
|
||||
auto attributes = Vertex::GetAttributeDescriptions();
|
||||
cmd.setVertexInputEXT(binding, attributes);
|
||||
|
||||
cmd.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1);
|
||||
vk::SampleMask sampleMask = 0xFFFFFFFF;
|
||||
cmd.setSampleMaskEXT(vk::SampleCountFlagBits::e1, sampleMask);
|
||||
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
|
||||
cmd.setAlphaToOneEnableEXT(VK_FALSE);
|
||||
|
||||
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);
|
||||
|
||||
vk::Rect2D scissor({0, 0}, ctx.extent);
|
||||
cmd.setScissorWithCount(scissor);
|
||||
|
||||
vk::Bool32 blendEnable = VK_FALSE;
|
||||
vk::ColorBlendEquationEXT blendEquation;
|
||||
blendEquation.setSrcColorBlendFactor(vk::BlendFactor::eOne)
|
||||
.setDstColorBlendFactor(vk::BlendFactor::eZero)
|
||||
.setColorBlendOp(vk::BlendOp::eAdd)
|
||||
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
|
||||
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
|
||||
.setAlphaBlendOp(vk::BlendOp::eAdd);
|
||||
|
||||
vk::ColorComponentFlags colorWriteMask =
|
||||
vk::ColorComponentFlagBits::eR |
|
||||
vk::ColorComponentFlagBits::eG |
|
||||
vk::ColorComponentFlagBits::eB |
|
||||
vk::ColorComponentFlagBits::eA;
|
||||
|
||||
// 两个颜色附件(HDR + velocity)都是无混合直写
|
||||
const vk::ColorBlendEquationEXT direct = MakeDirectEquation();
|
||||
cmd.setColorBlendEnableEXT(0,
|
||||
std::array<vk::Bool32, 2>{blendEnable, blendEnable});
|
||||
std::array<vk::Bool32, 2>{VK_TRUE, VK_FALSE});
|
||||
cmd.setColorBlendEquationEXT(0,
|
||||
std::array<vk::ColorBlendEquationEXT, 2>{blendEquation, blendEquation});
|
||||
std::array<vk::ColorBlendEquationEXT, 2>{premul, direct});
|
||||
cmd.setColorWriteMaskEXT(0,
|
||||
std::array<vk::ColorComponentFlags, 2>{colorWriteMask, colorWriteMask});
|
||||
std::array<vk::ColorComponentFlags, 2>{kColorWriteMask, kColorWriteMask});
|
||||
|
||||
// ---- 不透明批次:无混合直写,深度测试 + 写 ----
|
||||
for (const auto& batch : m_Frame.mainView.batches) {
|
||||
for (const auto& batch : m_Frame.translucentView.batches) {
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 1,
|
||||
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
|
||||
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
|
||||
}
|
||||
|
||||
// ---- 透明批次:预乘 alpha 混合(线性空间 over),深度只测
|
||||
// 不写,远 → 近排序(SceneRenderer 侧已按 batch 平均距离排)。
|
||||
// 附件 0(HDR)混合、附件 1(velocity)直写——透明表面的
|
||||
// velocity 就是它自身的运动,覆盖自己的像素即正确。
|
||||
// alpha 语义:frag 输出未预乘线性色 + 覆盖率 alpha,
|
||||
// 预乘在 tonemap 段(rgb * a)完成 —— hard constraint。
|
||||
if (!m_Frame.translucentView.batches.empty()) {
|
||||
vk::ColorBlendEquationEXT premul;
|
||||
premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
|
||||
.setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
|
||||
.setColorBlendOp(vk::BlendOp::eAdd)
|
||||
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
|
||||
.setDstAlphaBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
|
||||
.setAlphaBlendOp(vk::BlendOp::eAdd);
|
||||
|
||||
cmd.setColorBlendEnableEXT(0,
|
||||
std::array<vk::Bool32, 2>{VK_TRUE, VK_FALSE});
|
||||
cmd.setColorBlendEquationEXT(0,
|
||||
std::array<vk::ColorBlendEquationEXT, 2>{premul, blendEquation});
|
||||
cmd.setDepthWriteEnableEXT(VK_FALSE);
|
||||
|
||||
for (const auto& batch : m_Frame.translucentView.batches) {
|
||||
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
|
||||
m_Gpu.forwardLayout, 1,
|
||||
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
|
||||
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
|
||||
}
|
||||
|
||||
cmd.setDepthWriteEnableEXT(VK_TRUE); // 恢复(下帧 opaque 段也会重设)
|
||||
}
|
||||
|
||||
cmd.endRendering();
|
||||
})
|
||||
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
|
||||
.Write(m_Velocity, RG::ResourceUse::ColorAttachment)
|
||||
// depth 走写 use(WAW 边):保证透明段排在不透明段之后,
|
||||
// barrier 覆盖透明段对不透明深度的测试读(Read-Write hazard
|
||||
// 由 src=Write 的内存域保证可见性)
|
||||
.Write(m_Depth, RG::ResourceUse::DepthAttachment)
|
||||
.Read(m_ShadowMaps[0], RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
|
||||
@@ -174,6 +174,32 @@ namespace Engine::Passes
|
||||
cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
|
||||
cmd.setAlphaToOneEnableEXT(VK_FALSE);
|
||||
|
||||
// ---- 颜色混合必须显式直写 ----
|
||||
// shader-object 路径混合是动态状态,会从上一个 pass(ForwardTrans
|
||||
// 的 premul 混合)残留:tonemap 输出被二次混合 → rgb = color×a²
|
||||
//(拖低 alpha 平方级变黑),且 alpha 被残留方程与 clear(a=1) 混成
|
||||
// 恒 1 → 透明窗口永远不生效
|
||||
constexpr vk::Bool32 kBlendOff = VK_FALSE;
|
||||
constexpr vk::ColorComponentFlags kWriteAll =
|
||||
vk::ColorComponentFlagBits::eR |
|
||||
vk::ColorComponentFlagBits::eG |
|
||||
vk::ColorComponentFlagBits::eB |
|
||||
vk::ColorComponentFlagBits::eA;
|
||||
// 直写方程:(ONE, ZERO) → dst = src(无混合),alpha 同样直写
|
||||
static const vk::ColorBlendEquationEXT kDirect = [] {
|
||||
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;
|
||||
}();
|
||||
cmd.setColorBlendEnableEXT(0, kBlendOff);
|
||||
cmd.setColorBlendEquationEXT(0, kDirect);
|
||||
cmd.setColorWriteMaskEXT(0, kWriteAll);
|
||||
|
||||
vk::Viewport viewport(0.0f, 0.0f,
|
||||
static_cast<float>(ctx.extent.width),
|
||||
static_cast<float>(ctx.extent.height),
|
||||
|
||||
@@ -12,14 +12,28 @@ namespace Engine::Passes
|
||||
rg.AddPass("ImGui", [this](RG::RenderGraphContext& ctx) {
|
||||
const vk::CommandBuffer cmd = ctx.cmd;
|
||||
|
||||
// loadOp 按显示模式分支:Direct = blit 已铺满场景(Load 叠加);
|
||||
// Viewport = 本 pass 是唯一写入者(Clear 为"桌面底色")
|
||||
const bool direct = (m_Frame.displayMode == kDisplayDirect);
|
||||
|
||||
vk::ClearValue clearValue;
|
||||
clearValue.color = vk::ClearColorValue(
|
||||
std::array<float, 4>{0.04f, 0.045f, 0.06f, 1.0f});
|
||||
|
||||
vk::RenderingAttachmentInfo colorAttachment;
|
||||
colorAttachment.setImageView(ctx.View(m_Backbuffer))
|
||||
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
|
||||
.setLoadOp(vk::AttachmentLoadOp::eLoad) // 关键:叠加
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore);
|
||||
.setLoadOp(direct ? vk::AttachmentLoadOp::eLoad
|
||||
: vk::AttachmentLoadOp::eClear)
|
||||
.setStoreOp(vk::AttachmentStoreOp::eStore)
|
||||
.setClearValue(clearValue);
|
||||
|
||||
vk::RenderingInfo renderingInfo;
|
||||
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
|
||||
// 渲染区域 = backbuffer 物理尺寸(窗口大小),不是 ctx.extent
|
||||
//(= RG 目标尺寸 = 场景渲染分辨率)。Editor 视口模式下场景链
|
||||
// 按面板尺寸渲染,而 backbuffer 是全窗口——若跟随目标尺寸,
|
||||
// 整个 ImGui 会被裁剪到面板大小
|
||||
renderingInfo.setRenderArea({{0, 0}, ctx.Extent(m_Backbuffer)})
|
||||
.setLayerCount(1)
|
||||
.setColorAttachments(colorAttachment);
|
||||
|
||||
@@ -30,7 +44,11 @@ namespace Engine::Passes
|
||||
}
|
||||
|
||||
cmd.endRendering();
|
||||
}).Write(m_Backbuffer, RG::ResourceUse::ColorAttachment);
|
||||
}).Write(m_Backbuffer, RG::ResourceUse::ColorAttachment)
|
||||
// 场景纹理在本 pass 内被 ImGui::Image 的描述符采样:声明读让 RG
|
||||
// 补 ColorAttachment→ShaderRead barrier,并推导 eSampled usage
|
||||
.Read(m_PresentTarget, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment);
|
||||
}
|
||||
|
||||
} // namespace Engine::Passes
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
|
||||
#include <Engine/Renderer/Passes/SceneGpu.h>
|
||||
#include <Engine/Renderer/Material.h>
|
||||
#include <Engine/Renderer/Mesh.h> // IndirectGroup 取数(inline 经 Mesh)
|
||||
#include <Engine/Renderer/Cubemap.h>
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/Buffer.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
#include <Engine/Vulkan/ShaderManager.h>
|
||||
#include <Engine/Vulkan/UniformBuffer.h>
|
||||
@@ -15,14 +17,18 @@
|
||||
|
||||
namespace Engine::Passes
|
||||
{
|
||||
SceneGpu::SceneGpu(Device& device, DescriptorManager& descriptors,
|
||||
SceneGpu::SceneGpu(Device& device, MemoryAllocator& allocator,
|
||||
DescriptorManager& descriptors,
|
||||
ShaderManager& shaders,
|
||||
UniformBuffer& ubo, UniformBuffer& objectBuffer,
|
||||
UniformBuffer& prevObjectBuffer)
|
||||
: m_Device(device), m_Descriptors(descriptors)
|
||||
UniformBuffer& prevObjectBuffer,
|
||||
UniformBuffer& skinOffsetBuffer,
|
||||
UniformBuffer& skinPaletteBuffer)
|
||||
: m_Device(device), m_Allocator(allocator), m_Descriptors(descriptors)
|
||||
{
|
||||
// Set 0: UBO + GPUDriven 实例数据(SSBO,vertex 读取)
|
||||
// + 上一帧实例矩阵(TAA velocity,vertex 读取)
|
||||
// + 骨骼蒙皮:SkinOffsets(实例→palette 基址)+ SkinPalettes(骨骼矩阵)
|
||||
std::vector<DescriptorBinding> uboBindings = {
|
||||
{0, vk::DescriptorType::eUniformBuffer,
|
||||
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
|
||||
@@ -30,6 +36,10 @@ namespace Engine::Passes
|
||||
vk::ShaderStageFlagBits::eVertex},
|
||||
{2, vk::DescriptorType::eStorageBuffer,
|
||||
vk::ShaderStageFlagBits::eVertex},
|
||||
{3, vk::DescriptorType::eStorageBuffer,
|
||||
vk::ShaderStageFlagBits::eVertex},
|
||||
{4, vk::DescriptorType::eStorageBuffer,
|
||||
vk::ShaderStageFlagBits::eVertex},
|
||||
};
|
||||
uboSetLayout = descriptors.CreateLayout(uboBindings);
|
||||
|
||||
@@ -81,6 +91,11 @@ namespace Engine::Passes
|
||||
shaderDir + "simple_shader.frag.spv",
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
{uboSetLayout, textureSetLayout, shadowSetLayout}, 0);
|
||||
// 透明变体:同一套布局,frag 末尾写 gl_FragDepth=1.0
|
||||
auto transFrag = shaders.CreateFromFile(
|
||||
shaderDir + "simple_shader_translucent.frag.spv",
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
{uboSetLayout, textureSetLayout, shadowSetLayout}, 0);
|
||||
|
||||
// shadow shader:depth-only,push = lightVP
|
||||
auto shadowVertShader = shaders.CreateFromFile(
|
||||
@@ -96,10 +111,12 @@ namespace Engine::Passes
|
||||
|
||||
forwardVert = vert.Handle;
|
||||
forwardFrag = frag.Handle;
|
||||
translucentFrag = transFrag.Handle;
|
||||
shadowVert = shadowVertShader.Handle;
|
||||
shadowFrag = shadowFragShader.Handle;
|
||||
|
||||
if (!forwardVert || !forwardFrag || !shadowVert || !shadowFrag) {
|
||||
if (!forwardVert || !forwardFrag || !translucentFrag ||
|
||||
!shadowVert || !shadowFrag) {
|
||||
ENGINE_FATAL("SceneGpu: failed to create shader objects");
|
||||
}
|
||||
|
||||
@@ -132,10 +149,34 @@ namespace Engine::Passes
|
||||
descriptors.UpdateStorageBuffer(
|
||||
uboSets[slot], 2, prevObjectBuffer.Get(slot),
|
||||
prevObjectBuffer.GetSize());
|
||||
descriptors.UpdateStorageBuffer(
|
||||
uboSets[slot], 3, skinOffsetBuffer.Get(slot),
|
||||
skinOffsetBuffer.GetSize());
|
||||
descriptors.UpdateStorageBuffer(
|
||||
uboSets[slot], 4, skinPaletteBuffer.Get(slot),
|
||||
skinPaletteBuffer.GetSize());
|
||||
|
||||
shadowSets[slot] = descriptors.AllocateDescriptorSet(shadowSetLayout);
|
||||
}
|
||||
// shadowSets 的图片绑定在 Compile 后由 BindShadowMap 完成
|
||||
|
||||
// ---- MDI per-slot ring:命令 + count(CPU 直写,host visible)----
|
||||
// 布局 [main | casc0 | casc1 | casc2] × kMaxInstances 命令预算;
|
||||
// 两者 usage 都要 eIndirectBuffer(count buffer 的 spec 要求)
|
||||
const vk::DeviceSize cmdBytes = vk::DeviceSize(kIndirectViewMain + 1 + kMaxCascades) *
|
||||
kMaxInstances * sizeof(IndirectDrawCmd);
|
||||
const vk::DeviceSize cntBytes = vk::DeviceSize(kIndirectViewMain + 1 + kMaxCascades) *
|
||||
kMaxInstances * sizeof(u32);
|
||||
constexpr auto memProps = vk::MemoryPropertyFlagBits::eHostVisible |
|
||||
vk::MemoryPropertyFlagBits::eHostCoherent;
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
indirectCmdBuffers[slot] = CreateScope<Buffer>(
|
||||
device, allocator, cmdBytes,
|
||||
vk::BufferUsageFlagBits::eIndirectBuffer, memProps);
|
||||
indirectCountBuffers[slot] = CreateScope<Buffer>(
|
||||
device, allocator, cntBytes,
|
||||
vk::BufferUsageFlagBits::eIndirectBuffer, memProps);
|
||||
}
|
||||
}
|
||||
|
||||
SceneGpu::~SceneGpu()
|
||||
@@ -144,6 +185,7 @@ namespace Engine::Passes
|
||||
|
||||
if (forwardVert) vkDevice.destroyShaderEXT(forwardVert);
|
||||
if (forwardFrag) vkDevice.destroyShaderEXT(forwardFrag);
|
||||
if (translucentFrag) vkDevice.destroyShaderEXT(translucentFrag);
|
||||
if (shadowVert) vkDevice.destroyShaderEXT(shadowVert);
|
||||
if (shadowFrag) vkDevice.destroyShaderEXT(shadowFrag);
|
||||
|
||||
@@ -168,6 +210,52 @@ namespace Engine::Passes
|
||||
}
|
||||
}
|
||||
|
||||
// ---- MDI:per-slot 上传 + 组绘制 ----
|
||||
|
||||
void SceneGpu::UploadIndirect(u32 frameSlot, const FrameSceneData& frame)
|
||||
{
|
||||
auto uploadView = [&](u32 viewIdx, const CullResult& v) {
|
||||
if (v.indirectCommands.empty()) return;
|
||||
const vk::DeviceSize base =
|
||||
vk::DeviceSize(viewIdx) * kMaxInstances;
|
||||
indirectCmdBuffers[frameSlot]->Upload(
|
||||
v.indirectCommands.data(),
|
||||
v.indirectCommands.size() * sizeof(IndirectDrawCmd),
|
||||
base * sizeof(IndirectDrawCmd));
|
||||
// 每组一个 count,下标 = 组首命令下标(drawIndexedIndirectCount
|
||||
// 从此读取实际执行条数——GPU 剔除切片接管时只换写入者)
|
||||
for (const auto& g : v.indirectGroups) {
|
||||
indirectCountBuffers[frameSlot]->Upload(
|
||||
&g.commandCount, sizeof(u32),
|
||||
(base + g.firstCommand) * sizeof(u32));
|
||||
}
|
||||
};
|
||||
uploadView(kIndirectViewMain, frame.mainView);
|
||||
for (u32 c = 0; c < kMaxCascades; ++c) {
|
||||
uploadView(kIndirectViewMain + 1 + c, frame.shadowCascades[c]);
|
||||
}
|
||||
}
|
||||
|
||||
void SceneGpu::DrawIndirectGroup(vk::CommandBuffer cmd, u32 frameSlot,
|
||||
u32 viewIndex, const IndirectGroup& g)
|
||||
{
|
||||
// 组内全部命令共享同一 megabuffer:bind 一次,逐 mesh 定位由
|
||||
// 命令里的 firstIndex/vertexOffset 承担
|
||||
cmd.bindVertexBuffers(0, g.mesh->GetVertexBuffer(), {vk::DeviceSize(0)});
|
||||
cmd.bindIndexBuffer(g.mesh->GetIndexBuffer(), vk::DeviceSize(0),
|
||||
vk::IndexType::eUint32);
|
||||
const vk::DeviceSize base =
|
||||
vk::DeviceSize(viewIndex) * kMaxInstances;
|
||||
|
||||
cmd.drawIndexedIndirectCount(
|
||||
indirectCmdBuffers[frameSlot]->Get(),
|
||||
base * sizeof(IndirectDrawCmd),
|
||||
indirectCountBuffers[frameSlot]->Get(),
|
||||
(base + g.firstCommand) * sizeof(u32),
|
||||
g.commandCount, // maxDrawCount = CPU 打包的组大小
|
||||
sizeof(IndirectDrawCmd));
|
||||
}
|
||||
|
||||
void SceneGpu::BindEnvironment(const Cubemap& env)
|
||||
{
|
||||
// IBL 资源 view 全程稳定(不随 RG 重编译变化),绑一次即可。
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//
|
||||
|
||||
#include <Engine/Renderer/Passes/SkyboxPass.h>
|
||||
#include <Engine/Renderer/Passes/FullscreenGpu.h>
|
||||
#include <Engine/Renderer/Cubemap.h>
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
@@ -96,6 +97,8 @@ namespace Engine::Passes
|
||||
|
||||
const glm::vec4& cc = m_Frame.ClearColor;
|
||||
vk::ClearValue clear;
|
||||
// alpha 语义 = 窗口透过率(透明窗口与桌面合成;tonemap 段
|
||||
// rgb *= a 预乘)。滑条由用户控制:a<1 = 背景透出桌面
|
||||
clear.color = vk::ClearColorValue(
|
||||
std::array<float, 4>{cc.r, cc.g, cc.b, cc.a});
|
||||
|
||||
@@ -113,7 +116,8 @@ namespace Engine::Passes
|
||||
|
||||
cmd.beginRendering(renderingInfo);
|
||||
|
||||
if (m_Env && m_Frame.DrawSkybox) {
|
||||
if (m_Env && m_Frame.DrawSkybox &&
|
||||
m_Frame.debugView != Passes::kDebugTransparent) {
|
||||
// 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写)
|
||||
const std::array<vk::ShaderStageFlagBits, 2> stages = {
|
||||
vk::ShaderStageFlagBits::eVertex,
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Engine::Passes
|
||||
FrameSceneData& frame,
|
||||
RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
|
||||
RG::TextureHandle temporalDiff,
|
||||
RG::TextureHandle backbuffer,
|
||||
RG::TextureHandle presentTarget,
|
||||
RG::TextureHandle rawHdr,
|
||||
RG::TextureHandle velocity,
|
||||
RG::TextureHandle shadowMap0,
|
||||
@@ -22,7 +22,7 @@ namespace Engine::Passes
|
||||
RG::TextureHandle shadowMap2)
|
||||
: m_Gpu(gpu), m_Descriptors(descriptors), m_Params(params), m_Frame(frame),
|
||||
m_HDR(hdrColor), m_A(bloomA), m_TemporalDiff(temporalDiff),
|
||||
m_Backbuffer(backbuffer),
|
||||
m_Target(presentTarget),
|
||||
m_RawHDR(rawHdr), m_Velocity(velocity),
|
||||
m_ShadowMaps{shadowMap0, shadowMap1, shadowMap2}
|
||||
{
|
||||
@@ -38,7 +38,7 @@ namespace Engine::Passes
|
||||
{
|
||||
rg.AddPass("Tonemap",
|
||||
[this](RG::RenderGraphContext& ctx) {
|
||||
m_Gpu.RecordFullscreen(ctx, ctx.View(m_Backbuffer),
|
||||
m_Gpu.RecordFullscreen(ctx, ctx.View(m_Target),
|
||||
m_Gpu.tonemapFrag,
|
||||
m_Sets[ctx.frame.frameSlot],
|
||||
FullscreenPush{m_Params.exposure,
|
||||
@@ -49,7 +49,7 @@ namespace Engine::Passes
|
||||
static_cast<f32>(
|
||||
m_Frame.debugView)});
|
||||
})
|
||||
.Write(m_Backbuffer, RG::ResourceUse::ColorAttachment)
|
||||
.Write(m_Target, RG::ResourceUse::ColorAttachment)
|
||||
.Read(m_HDR, RG::ResourceUse::ShaderRead,
|
||||
vk::ShaderStageFlagBits::eFragment)
|
||||
.Read(m_A, RG::ResourceUse::ShaderRead,
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <Engine/Renderer/Passes/TemporalDiffPass.h>
|
||||
#include <Engine/Renderer/Passes/ImGuiPass.h>
|
||||
#include <Engine/Renderer/Passes/PresentPass.h>
|
||||
#include <Engine/Renderer/Passes/BlitPass.h>
|
||||
#include <Engine/Vulkan/Device.h>
|
||||
#include <Engine/Vulkan/MemoryAllocator.h>
|
||||
#include <Engine/Vulkan/DescriptorManager.h>
|
||||
@@ -34,6 +35,7 @@
|
||||
#include <Engine/Vulkan/UniformBuffer.h>
|
||||
#include <Engine/Core/Window.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/ImGui/ImGuiLayer.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
|
||||
@@ -43,6 +45,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
|
||||
namespace Engine
|
||||
{
|
||||
@@ -67,11 +70,22 @@ namespace Engine
|
||||
*m_Device, *m_Allocator,
|
||||
sizeof(glm::mat4) * kMaxInstances, kMaxFramesInFlight,
|
||||
vk::BufferUsageFlagBits::eStorageBuffer);
|
||||
// 骨骼蒙皮:offsets 与实例序平行(每实例一个 palette 基址);
|
||||
// palette 每实体一个 slot(帧首 bump 分配),容量 kMaxSkinMatrices
|
||||
m_SkinOffsetBuffer = CreateScope<UniformBuffer>(
|
||||
*m_Device, *m_Allocator,
|
||||
sizeof(u32) * kMaxInstances, kMaxFramesInFlight,
|
||||
vk::BufferUsageFlagBits::eStorageBuffer);
|
||||
m_SkinPaletteBuffer = CreateScope<UniformBuffer>(
|
||||
*m_Device, *m_Allocator,
|
||||
sizeof(glm::mat4) * kMaxSkinMatrices, kMaxFramesInFlight,
|
||||
vk::BufferUsageFlagBits::eStorageBuffer);
|
||||
|
||||
// 3. pass 装配体:布局/shader/采样器/per-slot 描述符
|
||||
m_SceneGpu = CreateScope<Passes::SceneGpu>(
|
||||
*m_Device, *m_DescriptorManager, *m_ShaderManager,
|
||||
*m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer);
|
||||
*m_Device, *m_Allocator, *m_DescriptorManager, *m_ShaderManager,
|
||||
*m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer,
|
||||
*m_SkinOffsetBuffer, *m_SkinPaletteBuffer);
|
||||
m_FullscreenGpu = CreateScope<Passes::FullscreenGpu>(
|
||||
*m_Device, *m_DescriptorManager, *m_ShaderManager);
|
||||
|
||||
@@ -191,6 +205,12 @@ namespace Engine
|
||||
int Renderer::GetDebugView() const { return m_SceneRenderer->Frame().debugView; }
|
||||
void Renderer::SetDebugView(int v) { m_SceneRenderer->SetDebugView(v); }
|
||||
|
||||
void Renderer::SetDisplayMode(int mode)
|
||||
{
|
||||
m_DesiredDisplayMode = mode;
|
||||
m_SceneRenderer->SetDisplayMode(mode);
|
||||
}
|
||||
|
||||
void Renderer::SetEnvironment(const Cubemap* env)
|
||||
{
|
||||
if (!env) env = m_Environment.get();
|
||||
@@ -210,9 +230,9 @@ namespace Engine
|
||||
static const std::vector<const char*> kNames = {
|
||||
"Final", "TemporalDiff", "HDR", "TAAColor", "Bloom",
|
||||
"Velocity", "ShadowMap0", "ShadowMap1", "ShadowMap2",
|
||||
"EnvCube", "Irradiance", "Prefilter", "BrdfLut",
|
||||
"EnvCube", "Irradiance", "Prefilter", "BrdfLut", "Transparent",
|
||||
};
|
||||
static_assert(int(Passes::kDebugViewCount) == 13,
|
||||
static_assert(int(Passes::kDebugViewCount) == 14,
|
||||
"DebugView 枚举与名表同步");
|
||||
return kNames;
|
||||
}
|
||||
@@ -244,9 +264,17 @@ namespace Engine
|
||||
|
||||
// ---- 资源声明(命名资源;pass 经句柄引用,数据流由 uses 决定)----
|
||||
|
||||
// 导入 swapchain 颜色附件
|
||||
// 导入 swapchain 颜附件(物理尺寸 = 窗口,与场景渲染分辨率解耦)
|
||||
m_Backbuffer = m_RenderGraph->ImportSwapchain(
|
||||
"Backbuffer", m_Swapchain->GetFormat());
|
||||
m_RenderGraph->SetImportedExtent(m_Backbuffer, m_Swapchain->GetExtent());
|
||||
|
||||
// 显示目标:tonemap 写这里(offscreen),ImGui::Image 采样后自由
|
||||
// 摆放;backbuffer 只承载 ImGui。格式随 swapchain(Unorm:tonemap
|
||||
// 自带 gamma 编码,采样 → ImGui 写回全程无二次编解码,零色偏)
|
||||
m_PresentTarget = m_RenderGraph->CreateTexture(
|
||||
"PresentTarget",
|
||||
RG::TextureDesc{m_Swapchain->GetFormat()});
|
||||
|
||||
// 深度图:目标尺寸,格式由 Device 能力决定
|
||||
const vk::Format depthFormat = m_Device->FindSupportedFormat(
|
||||
@@ -317,10 +345,15 @@ namespace Engine
|
||||
m_BloomB));
|
||||
m_Passes.push_back(CreateScope<Passes::TonemapPass>(
|
||||
*m_FullscreenGpu, *m_DescriptorManager, *m_PostParams, frame,
|
||||
m_TAAColor, m_BloomA, m_TemporalDiff, m_Backbuffer, m_HDRColor,
|
||||
m_TAAColor, m_BloomA, m_TemporalDiff, m_PresentTarget, m_HDRColor,
|
||||
m_Velocity, m_ShadowMaps[0], m_ShadowMaps[1], m_ShadowMaps[2]));
|
||||
// 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, m_Backbuffer));
|
||||
&m_ImGuiLayer, frame, m_Backbuffer, m_PresentTarget));
|
||||
m_Passes.push_back(CreateScope<Passes::PresentPass>(m_Backbuffer));
|
||||
|
||||
for (auto& pass : m_Passes) {
|
||||
@@ -330,29 +363,88 @@ namespace Engine
|
||||
m_RenderGraph->SetTargetExtent(m_Extent); // 触发首次 Compile
|
||||
}
|
||||
|
||||
void Renderer::SetVSync(bool vsync)
|
||||
// ---- 帧首同步:所有"会换物理资源/描述符"的意图统一在这里落定 ----
|
||||
|
||||
vk::Extent2D Renderer::DesiredRenderExtent() const
|
||||
{
|
||||
if (m_Swapchain->GetVSync() == vsync) return;
|
||||
m_Swapchain->SetVSync(vsync);
|
||||
HandleResize(); // present mode 变化需要重建 swapchain
|
||||
// Editor 视口模式:渲染分辨率跟随 ImGui 面板(面板未上报/关闭时
|
||||
// 回退 swapchain 尺寸);Direct 模式:全窗口
|
||||
return (m_DesiredDisplayMode == kDisplayViewport && m_DesiredViewport.width)
|
||||
? m_DesiredViewport
|
||||
: m_Swapchain->GetExtent();
|
||||
}
|
||||
|
||||
void Renderer::HandleResize()
|
||||
void Renderer::BeginFrameSync()
|
||||
{
|
||||
const bool vsyncChanged = (m_DesiredVSync != m_Swapchain->GetVSync());
|
||||
const bool windowResized = m_Window.WasResized();
|
||||
const vk::Extent2D want = DesiredRenderExtent();
|
||||
const bool extentChanged =
|
||||
(want.width != m_Extent.width || want.height != m_Extent.height);
|
||||
|
||||
if (!vsyncChanged && !windowResized && !extentChanged) return;
|
||||
|
||||
if (windowResized) m_Window.ResetResizedFlag();
|
||||
// swapchain 重建仅由窗口 resize / vsync 切换触发;
|
||||
// 面板拖拽只重编译 RG(swapchain 与渲染分辨率解耦)
|
||||
HandleResize(windowResized || vsyncChanged);
|
||||
}
|
||||
|
||||
// GPU 空闲 → (可选)重建 swapchain → RG 重编译到期望渲染分辨率 →
|
||||
// 各 pass 重绑输入描述符 → 场景纹理重新注册到 ImGui。
|
||||
// 调用点:BeginFrameSync(UI 构建前)与帧中止路径(acquire/present
|
||||
// 失败)——都不存在"已构建 draw data 引用旧描述符"的窗口
|
||||
void Renderer::HandleResize(bool recreateSwapchain)
|
||||
{
|
||||
m_Device->GetDevice().waitIdle();
|
||||
|
||||
m_Swapchain->Recreate(m_Window);
|
||||
m_Extent = m_Swapchain->GetExtent();
|
||||
if (recreateSwapchain) {
|
||||
// 先把 vsync 意图落进 swapchain(Create 按 m_VSync 选 present
|
||||
// mode 且从不自行修改它)——否则 vsyncChanged 每帧恒真,
|
||||
// swapchain 被无限重建,present mode 也永远切不过去
|
||||
m_Swapchain->SetVSync(m_DesiredVSync);
|
||||
m_Swapchain->Recreate(m_Window);
|
||||
// image 数量可能变化
|
||||
m_SyncManager = CreateScope<SyncManager>(
|
||||
*m_Device, kMaxFramesInFlight, m_Swapchain->GetImageCount());
|
||||
// backbuffer 物理尺寸跟新窗口走(ImGui 渲染区域 = 全窗口)
|
||||
m_RenderGraph->SetImportedExtent(m_Backbuffer,
|
||||
m_Swapchain->GetExtent());
|
||||
}
|
||||
|
||||
// image 数量可能变化
|
||||
m_SyncManager = CreateScope<SyncManager>(
|
||||
*m_Device, kMaxFramesInFlight, m_Swapchain->GetImageCount());
|
||||
m_Extent = DesiredRenderExtent(); // 重建后取(swapchain 尺寸已新鲜)
|
||||
|
||||
// RG 重编译:物理镜像按新尺寸轮换,各 pass 重绑输入描述符
|
||||
m_RenderGraph->SetTargetExtent(m_Extent);
|
||||
for (auto& pass : m_Passes) {
|
||||
pass->OnTargetsRebuilt(*m_RenderGraph);
|
||||
}
|
||||
RefreshSceneTextures();
|
||||
}
|
||||
|
||||
// ---- 场景显示纹理(PresentTarget → ImGui::Image)----
|
||||
|
||||
void* Renderer::GetSceneTexture() const
|
||||
{
|
||||
// UI 构建段时 m_CurrentFrame 已指向本帧 Render 将使用的槽位
|
||||
//(上一帧 Render 尾部推进),UI 与渲染同槽位
|
||||
void* tex = nullptr;
|
||||
if (m_ImGuiLayer) {
|
||||
tex = (void*)(intptr_t)
|
||||
m_ImGuiLayer->GetSceneTexture(m_CurrentFrame);
|
||||
}
|
||||
return tex;
|
||||
}
|
||||
|
||||
// RG 物理镜像按帧槽位各一份:每槽位注册一个 ImGui 描述符。
|
||||
// 首次编译后(SetImGuiLayer)与每次重编译后(HandleResize)调用
|
||||
void Renderer::RefreshSceneTextures()
|
||||
{
|
||||
if (!m_ImGuiLayer || !m_PresentTarget.Valid()) return;
|
||||
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
|
||||
m_ImGuiLayer->UpdateSceneTexture(
|
||||
slot, m_RenderGraph->GetPhysicalView(m_PresentTarget, slot));
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::ReadGpuTimes(u32 frameSlot)
|
||||
@@ -386,19 +478,16 @@ namespace Engine
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets)
|
||||
void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets, f32 dt)
|
||||
{
|
||||
ZoneScoped; // CPU 侧:UBO/SSBO 上传 + 录制 + 提交(GPU 耗时见 RG timestamp)
|
||||
if (m_Window.WasResized()) {
|
||||
m_Window.ResetResizedFlag();
|
||||
HandleResize();
|
||||
}
|
||||
// resize 已在 BeginFrameSync(UI 构建前)处理,此处无需再查
|
||||
|
||||
const u32 frameSlot = m_CurrentFrame;
|
||||
const auto& vkDevice = m_Device->GetDevice();
|
||||
|
||||
// 1. 场景 → 当帧渲染数据(相机/灯光/双视图剔除;纯 CPU,不碰 GPU)
|
||||
if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent)) {
|
||||
if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent, dt)) {
|
||||
return; // 无 primary 相机:跳帧
|
||||
}
|
||||
const FrameSceneData& frame = m_SceneRenderer->Frame();
|
||||
@@ -417,6 +506,18 @@ namespace Engine
|
||||
frame.prevInstanceModels.size() *
|
||||
sizeof(glm::mat4));
|
||||
}
|
||||
// 蒙皮数据与实例矩阵同窗口落槽位 ring(fence 已等,写-读竞争关闭)
|
||||
if (!frame.skinOffsets.empty()) {
|
||||
m_SkinOffsetBuffer->Update(frameSlot, frame.skinOffsets.data(),
|
||||
frame.skinOffsets.size() * sizeof(u32));
|
||||
}
|
||||
if (!frame.skinPalettes.empty()) {
|
||||
m_SkinPaletteBuffer->Update(frameSlot, frame.skinPalettes.data(),
|
||||
frame.skinPalettes.size() *
|
||||
sizeof(glm::mat4));
|
||||
}
|
||||
// MDI 命令/count 同窗口落槽位 ring(fence 已等,写-读竞争关闭)
|
||||
m_SceneGpu->UploadIndirect(frameSlot, frame);
|
||||
|
||||
// 3. acquire swapchain image
|
||||
u32 imageIndex = 0;
|
||||
@@ -427,13 +528,13 @@ namespace Engine
|
||||
m_SyncManager->GetImageAvailable(frameSlot));
|
||||
|
||||
if (result.result == vk::Result::eErrorOutOfDateKHR) {
|
||||
HandleResize();
|
||||
HandleResize(true); // 帧中止:无 draw data 引用旧资源,安全
|
||||
return;
|
||||
}
|
||||
imageIndex = result.value;
|
||||
}
|
||||
catch (const vk::OutOfDateKHRError&) {
|
||||
HandleResize();
|
||||
HandleResize(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -486,11 +587,11 @@ namespace Engine
|
||||
try {
|
||||
const auto result = m_Device->GetPresentQueue().presentKHR(presentInfo);
|
||||
if (result == vk::Result::eSuboptimalKHR) {
|
||||
HandleResize();
|
||||
HandleResize(true);
|
||||
}
|
||||
}
|
||||
catch (const vk::OutOfDateKHRError&) {
|
||||
HandleResize();
|
||||
HandleResize(true);
|
||||
}
|
||||
|
||||
m_CurrentFrame = (m_CurrentFrame + 1) % kMaxFramesInFlight;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
@@ -46,6 +47,121 @@ 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)
|
||||
@@ -132,7 +248,7 @@ namespace Engine
|
||||
}
|
||||
|
||||
bool SceneRenderer::BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
|
||||
vk::Extent2D targetExtent)
|
||||
vk::Extent2D targetExtent, f32 dt)
|
||||
{
|
||||
ZoneScopedN("BuildFrame");
|
||||
const auto collectStart = std::chrono::steady_clock::now();
|
||||
@@ -179,6 +295,7 @@ namespace Engine
|
||||
m_Frame.bloomEnabled = m_BloomEnabled;
|
||||
m_Frame.lodEnabled = m_LodEnabled;
|
||||
m_Frame.debugView = m_DebugView;
|
||||
m_Frame.displayMode = m_DisplayMode;
|
||||
ubo.cascadeSplits.w = m_ShadowsEnabled ? 1.0f : 0.0f;
|
||||
|
||||
// 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass;
|
||||
@@ -250,6 +367,107 @@ namespace Engine
|
||||
ComputeCascades(shadowDir);
|
||||
ubo.cameraPos = glm::vec4(camTransform->GetPosition(), 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) {
|
||||
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);
|
||||
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; // 过渡完成
|
||||
} else {
|
||||
src = &clips[anim.fadeFrom];
|
||||
anim.fadeFromTime = wrapTime(anim.fadeFromTime, *src);
|
||||
srcTime = anim.fadeFromTime;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 采样 + 局部 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;
|
||||
});
|
||||
|
||||
// ---- 3. GPUDriven:单趟收集 + 多视图剔除 ----
|
||||
// 同作用域第二个 zone:ZoneScopedN 变量名固定会重声明,用 ZoneNamedN
|
||||
ZoneNamedN( ___tracy_collect, "GPUDriven Collect", true );
|
||||
@@ -262,8 +480,10 @@ namespace Engine
|
||||
// batch.first 拼接时加上段基址,保证段内区间连续。
|
||||
m_Frame.instanceModels.clear();
|
||||
m_Frame.prevInstanceModels.clear();
|
||||
m_Frame.skinOffsets.clear();
|
||||
m_MainScratch.Clear();
|
||||
for (auto& s : m_CascScratch) s.Clear();
|
||||
m_TranslucentScratch.Clear(); // 透明队列每帧重排批次,必须整帧重建
|
||||
u32 totalInstances = 0;
|
||||
bool instanceTruncated = false;
|
||||
|
||||
@@ -289,6 +509,13 @@ namespace Engine
|
||||
}
|
||||
m_CurWorlds.emplace(entKey, t.World());
|
||||
|
||||
// palette 基址(第 2.5 节预采样;无动画实体 = identity 槽)
|
||||
u32 skinBase = kIdentityPaletteSlot;
|
||||
if (auto it = m_EntitySkinBase.find(entKey);
|
||||
it != m_EntitySkinBase.end()) {
|
||||
skinBase = it->second;
|
||||
}
|
||||
|
||||
// 实例世界缩放(矩阵列向量最大长度,包围球半径随缩放)
|
||||
const f32 worldScale = glm::max(
|
||||
glm::length(glm::vec3(t.World()[0])),
|
||||
@@ -359,7 +586,7 @@ namespace Engine
|
||||
// 实例在扁平化后连续——firstInstance 语义的前提
|
||||
const u32 bi = batchIdx - 1;
|
||||
s.result.batches[bi].count++;
|
||||
s.batchMats[bi].push_back({t.World(), prevWorld});
|
||||
s.batchMats[bi].push_back({t.World(), prevWorld, skinBase});
|
||||
s.batchDistSum[bi] += dist; // 透明排序用
|
||||
s.result.instanceCount++;
|
||||
++totalInstances;
|
||||
@@ -398,6 +625,8 @@ namespace Engine
|
||||
for (const auto& m : s.batchMats[i]) {
|
||||
m_Frame.instanceModels.push_back(m.cur);
|
||||
m_Frame.prevInstanceModels.push_back(m.prev);
|
||||
// 与实例序平行对齐(shader 按 gl_InstanceIndex 取)
|
||||
m_Frame.skinOffsets.push_back(m.skinBase);
|
||||
}
|
||||
}
|
||||
dst.instanceCount = s.result.instanceCount;
|
||||
@@ -407,6 +636,12 @@ namespace Engine
|
||||
appendView(m_Frame.shadowCascades[c], m_CascScratch[c]);
|
||||
}
|
||||
|
||||
// ---- 4.5 MDI 打包(已下线:移动视角下存在几何消失缺陷,待隔离修复)----
|
||||
// PackIndirect(m_Frame.mainView, true);
|
||||
// for (u32 c = 0; c < kMaxCascades; ++c) {
|
||||
// PackIndirect(m_Frame.shadowCascades[c], false);
|
||||
// }
|
||||
|
||||
// ---- 5. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排
|
||||
{
|
||||
auto& tb = m_TranslucentScratch.result.batches;
|
||||
@@ -466,4 +701,52 @@ namespace Engine
|
||||
return true;
|
||||
}
|
||||
|
||||
void SceneRenderer::PackIndirect(CullResult& view, bool byMaterial)
|
||||
{
|
||||
view.indirectCommands.clear();
|
||||
view.indirectGroups.clear();
|
||||
if (view.batches.empty()) return;
|
||||
|
||||
// 组键排序:megabuffer(VB 句柄)→ 材质(可选)→ mesh 槽位。
|
||||
// 同 Model 的全部 mesh 共享同一 megabuffer,材质是主分组轴;
|
||||
// 槽位兜底保证排序稳定(组内顺序无语义,防逐帧抖动)
|
||||
std::sort(view.batches.begin(), view.batches.end(),
|
||||
[byMaterial](const InstanceBatch& a, const InstanceBatch& b) {
|
||||
const u64 va = reinterpret_cast<u64>(static_cast<VkBuffer>(
|
||||
a.mesh->GetVertexBuffer()));
|
||||
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());
|
||||
}
|
||||
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;
|
||||
|
||||
bool newGroup = view.indirectGroups.empty();
|
||||
if (!newGroup) {
|
||||
const IndirectGroup& g = view.indirectGroups.back();
|
||||
newGroup = g.mesh->GetVertexBuffer() != vb ||
|
||||
g.material != mat;
|
||||
}
|
||||
if (newGroup) {
|
||||
view.indirectGroups.push_back({b.mesh, mat,
|
||||
static_cast<u32>(view.indirectCommands.size()), 0});
|
||||
}
|
||||
|
||||
view.indirectCommands.push_back({
|
||||
b.mesh->GetLodIndexCount(b.lod),
|
||||
b.count,
|
||||
b.mesh->GetGlobalFirstIndex(b.lod),
|
||||
b.mesh->GetVertexOffset(),
|
||||
b.first});
|
||||
++view.indirectGroups.back().commandCount;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Engine
|
||||
|
||||
Reference in New Issue
Block a user