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:
@@ -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,57 +38,66 @@ 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(transform.GetRotationQuat());
|
||||
const glm::vec3 delta =
|
||||
(-r * glm::vec3(1, 0, 0)) * mouseDX +
|
||||
(r * glm::vec3(0, 1, 0)) * mouseDY;
|
||||
const f32 k = m_OrbitDistance * 0.002f; // m/px
|
||||
transform.SetPosition(transform.GetPosition() + delta * k);
|
||||
if (m_HasPivot) m_OrbitPivot += delta * k;
|
||||
}
|
||||
else
|
||||
{
|
||||
// ---- 焦点环绕:世界系左乘增量(yaw 绕世界 Y、pitch
|
||||
// 绕相机右轴),roll 保持不变;位置绕 pivot 同
|
||||
// 角度刚体旋转 → 到 pivot 距离恒定。首次环绕无
|
||||
// pivot:取当前视线上 m_OrbitDistance 处 ----
|
||||
if (!m_HasPivot)
|
||||
{
|
||||
// ---- 屏幕平面平移:内容跟手(拖右 → 相机左移、
|
||||
// 拖下 → 相机上移),pivot 同步平移保持环绕点的
|
||||
// 屏幕相对位置 ----
|
||||
const glm::mat3 r = EulerRotMat(rot);
|
||||
const glm::vec3 delta =
|
||||
(-r * glm::vec3(1, 0, 0)) * mouseDX +
|
||||
(r * glm::vec3(0, 1, 0)) * mouseDY;
|
||||
const f32 k = m_OrbitDistance * 0.002f; // m/px
|
||||
transform.SetPosition(transform.GetPosition() + delta * k);
|
||||
if (m_HasPivot) m_OrbitPivot += delta * k;
|
||||
}
|
||||
else
|
||||
{
|
||||
// ---- 焦点环绕:转向后保持到 pivot 的距离。首次
|
||||
// 环绕无 pivot:取当前视线上 m_OrbitDistance 处
|
||||
if (!m_HasPivot)
|
||||
{
|
||||
m_OrbitPivot = transform.GetPosition() +
|
||||
EulerRotMat(transform.GetRotation()) *
|
||||
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::mat3 r(transform.GetRotationQuat());
|
||||
m_OrbitPivot = transform.GetPosition() +
|
||||
(r * glm::vec3(0, 0, -1)) *
|
||||
m_OrbitDistance;
|
||||
m_HasPivot = true;
|
||||
}
|
||||
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);
|
||||
proj[1][1] *= -1.0f; // Vulkan Y 翻转
|
||||
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;
|
||||
// Vulkan clip 深度 [0,1]:z' = 0.5*z_clip + 0.5*w_clip。
|
||||
// perspective 的 w_clip = -z_eye,偏移折算进 z 系数
|
||||
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
|
||||
+177
-19
@@ -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,12 +699,46 @@ 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) {
|
||||
texPaths[slot] = (modelDir / path.C_Str()).string();
|
||||
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; // 过渡完成
|
||||
} 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;
|
||||
});
|
||||
#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 {
|
||||
m_Frame.gizmoIconVerts.clear();
|
||||
m_Frame.gizmoWireVerts.clear();
|
||||
m_Frame.gizmoProxyVerts.clear();
|
||||
// 骨骼可视化数组与 gizmo 同批清(AnimationSystem.Update 开头
|
||||
// 也会清,此处双保险:PIE 态/全屏直渲绝无残留绘制)
|
||||
m_Frame.boneWireVerts.clear();
|
||||
m_Frame.boneJoints.clear();
|
||||
}
|
||||
#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)
|
||||
const u32 slot = mesh->GetSlot();
|
||||
if (slot >= s.lookup.size()) {
|
||||
s.lookup.resize(slot + 1);
|
||||
// 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);
|
||||
}
|
||||
lodSlots = &s.lookup[slot];
|
||||
}
|
||||
auto& lodSlots = s.lookup[slot];
|
||||
u32 batchIdx = lodSlots[lod];
|
||||
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
|
||||
Reference in New Issue
Block a user