From f1078542360f224c964dbcc7883c15b5883cb98a Mon Sep 17 00:00:00 2001 From: atdunbg Date: Sun, 18 Oct 2026 17:09:25 +0800 Subject: [PATCH] =?UTF-8?q?MeshFactory=EF=BC=9A=E7=A8=8B=E5=BA=8F=E5=8C=96?= =?UTF-8?q?=E5=87=A0=E4=BD=95=E5=8D=95=E4=B8=80=E6=BA=90=20+=20CreatePrimi?= =?UTF-8?q?tive=20=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 MeshFactory(Cube/Plane/Sphere/Cylinder/Cone/Torus + 切线计算), Model 程序化几何改走 MeshFactory(收敛为单一几何源,消除重复的顶点 生成),并新增 CreatePrimitive;打通 AssetManager/IAssetUploader/ VulkanAssetUploader 的 CreatePrimitive 链路;编辑器 Procedural 弹窗 下拉 + ResolveProcedural 伪路径反解 + 场景序列化(程序化图元可存取)。 --- App/editor/panels/InspectorPanel.cpp | 68 +++- Engine/include/Engine/Assets/AssetManager.h | 6 + Engine/include/Engine/Assets/IAssetUploader.h | 6 + Engine/include/Engine/Renderer/MeshFactory.h | 72 ++++ Engine/include/Engine/Renderer/Model.h | 36 +- .../Engine/Renderer/VulkanAssetUploader.h | 3 + Engine/src/Assets/AssetManager.cpp | 27 ++ Engine/src/Renderer/MeshFactory.cpp | 364 ++++++++++++++++++ Engine/src/Renderer/Model.cpp | 130 ++----- Engine/src/Renderer/VulkanAssetUploader.cpp | 8 + Engine/src/Serialization/SceneSerializer.cpp | 51 +++ 11 files changed, 654 insertions(+), 117 deletions(-) create mode 100644 Engine/include/Engine/Renderer/MeshFactory.h create mode 100644 Engine/src/Renderer/MeshFactory.cpp diff --git a/App/editor/panels/InspectorPanel.cpp b/App/editor/panels/InspectorPanel.cpp index 28f1bd8..1b8adfb 100644 --- a/App/editor/panels/InspectorPanel.cpp +++ b/App/editor/panels/InspectorPanel.cpp @@ -686,8 +686,13 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) } if (ImGui::BeginPopup("##proc_model")) { - static int shape = 0; // 0=cube 1=plane 2=transparent plane - static float half = 0.5f; + // 形状集合与 MeshFactory::ShapeNames 对齐 + static int shape = 0; // 0=cube 1=plane 2=tplane + // 3=sphere 4=cylinder 5=cone 6=torus + static float half = 0.5f; // 半径 / 半边长 + static float height = 1.0f; // 圆柱 / 圆锥高 + static float majorR = 0.5f; // 圆环主半径 + static float minorR = 0.15f; // 圆环管半径 static float col[3] = {0.55f, 0.36f, 0.28f}; static float alpha = 0.5f; ImGui::RadioButton("Cube", &shape, 0); @@ -695,11 +700,32 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) ImGui::RadioButton("Plane", &shape, 1); ImGui::SameLine(); ImGui::RadioButton("Trans.Plane", &shape, 2); + ImGui::RadioButton("Sphere", &shape, 3); + ImGui::SameLine(); + ImGui::RadioButton("Cylinder", &shape, 4); + ImGui::SameLine(); + ImGui::RadioButton("Cone", &shape, 5); + ImGui::SameLine(); + ImGui::RadioButton("Torus", &shape, 6); // 固定宽度而非 -1:弹窗是 AlwaysAutoResize,-1(撑满可用 // 宽度)会拿上一帧宽度当基准 → 逐帧膨胀直到撑满屏幕 ImGui::SetNextItemWidth(180.0f); - ImGui::SliderFloat("half size", &half, 0.01f, 10.0f, - "%.2f"); + ImGui::SliderFloat("size", &half, 0.01f, 10.0f, "%.2f"); + if (shape == 4 || shape == 5) + { + ImGui::SetNextItemWidth(180.0f); + ImGui::SliderFloat("height", &height, 0.01f, 10.0f, + "%.2f"); + } + if (shape == 6) + { + ImGui::SetNextItemWidth(180.0f); + ImGui::SliderFloat("major radius", &majorR, 0.01f, 10.0f, + "%.2f"); + ImGui::SetNextItemWidth(180.0f); + ImGui::SliderFloat("minor radius", &minorR, 0.01f, 5.0f, + "%.2f"); + } ImGui::ColorEdit3("color", col); if (shape == 2) { @@ -712,21 +738,37 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) const int r = int(col[0] * 255.0f + 0.5f); const int g = int(col[1] * 255.0f + 0.5f); const int b = int(col[2] * 255.0f + 0.5f); - char key[96]; - if (shape == 2) + char key[128]; + switch (shape) { - const int a = int(alpha * 255.0f + 0.5f); + case 2: std::snprintf( key, sizeof(key), "procedural://transparent-plane/%.3g/%d,%d,%d,%d", - half, r, g, b, a); - } - else - { - std::snprintf( - key, sizeof(key), + half, r, g, b, int(alpha * 255.0f + 0.5f)); + break; + case 3: + std::snprintf(key, sizeof(key), + "procedural://sphere/%.3g/%d,%d,%d", + half, r, g, b); + break; + case 4: + case 5: + std::snprintf(key, sizeof(key), + "procedural://%s/%.3g_%.3g/%d,%d,%d", + shape == 4 ? "cylinder" : "cone", + half, height, r, g, b); + break; + case 6: + std::snprintf(key, sizeof(key), + "procedural://torus/%.3g_%.3g/%d,%d,%d", + majorR, minorR, r, g, b); + break; + default: + std::snprintf(key, sizeof(key), "procedural://%s/%.3g/%d,%d,%d", shape == 0 ? "cube" : "plane", half, r, g, b); + break; } if (auto ref = Engine::Serialization:: SceneSerializer::ResolveProcedural( diff --git a/Engine/include/Engine/Assets/AssetManager.h b/Engine/include/Engine/Assets/AssetManager.h index 63f7b83..9a9e60d 100644 --- a/Engine/include/Engine/Assets/AssetManager.h +++ b/Engine/include/Engine/Assets/AssetManager.h @@ -229,6 +229,12 @@ namespace Engine // 尺寸变化走实体 Transform 缩放,勿按尺寸炸缓存 ModelRef CreateCube(f32 halfSize, u8 r, u8 g, u8 b); + // 通用程序化图元(MeshFactory 形状名 + 参数):同形状同参数同色 + // 缓存去重。几何唯一源 = MeshFactory(见 MeshFactory.h) + ModelRef CreatePrimitive(const std::string& shape, + const PrimitiveDesc& desc, + u8 r, u8 g, u8 b); + // 环境资产(HDR → IBL 预积分四件套)。路径缓存去重, // 重复 Load 同一路径零成本(动态切换场景直接反复调用) EnvRef LoadEnvironment(const std::string& path); diff --git a/Engine/include/Engine/Assets/IAssetUploader.h b/Engine/include/Engine/Assets/IAssetUploader.h index f26b898..db57ba8 100644 --- a/Engine/include/Engine/Assets/IAssetUploader.h +++ b/Engine/include/Engine/Assets/IAssetUploader.h @@ -45,6 +45,12 @@ namespace Engine::Assets { // 程序化纯色立方体(halfSize 为半边长;占位几何/碎片模拟) virtual Scope CreateCube(f32 halfSize, u8 r, u8 g, u8 b) = 0; + // 通用程序化图元(MeshFactory 形状名:cube/plane/sphere/cylinder/ + // cone/torus 等):形状 + 参数 → 单 mesh 模型。非法形状返回 nullptr + virtual Scope CreatePrimitive(const std::string& shape, + const PrimitiveDesc& desc, + u8 r, u8 g, u8 b) = 0; + // 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr) virtual Scope LoadEnvironment(const std::string& path) = 0; diff --git a/Engine/include/Engine/Renderer/MeshFactory.h b/Engine/include/Engine/Renderer/MeshFactory.h new file mode 100644 index 0000000..94294ac --- /dev/null +++ b/Engine/include/Engine/Renderer/MeshFactory.h @@ -0,0 +1,72 @@ +// +// Created by atdunbg on 2026/10/18. +// +// MeshFactory:预制图元的 CPU 几何生成端(唯一几何源)。 +// +// 定位对齐 UE 的 Engine\Source\Runtime\Engine\...(内置基础网格)思路: +// 引擎只保留一套"参数化图元 → 顶点/索引"的生成器,程序化 Model、 +// 物理形状、脚本与编辑器下拉全部复用它,避免各路径各写一份立方体。 +// +// 契约: +// - 所有图元中心在局部原点(平面在 y=0 的 XZ 面),尺寸为世界单位, +// 尺寸变化统一走实体 Transform 缩放,不按尺寸炸资产缓存; +// - 面法线朝外、CCW 从外正视,可被背面剔除; +// - 切线已按 UV 正交化(法线贴图 TBN 可用),退化为任一与 N 正交向量; +// - 输出 LOD0 单级索引(图元顶点少,无简化链价值)。 +// + +#ifndef HELLOVULKAN_MESHFACTORY_H +#define HELLOVULKAN_MESHFACTORY_H + +#include +#include + +#include +#include + +namespace Engine { + + // 图元参数(形状无关的标量集):各形状只读取自己关心的字段。 + // 默认值给"半径/半边长 0.5、高 1"的规格化尺寸,贴合单位立方体约定。 + struct PrimitiveDesc { + f32 size = 0.5f; // 半边长(cube/plane)/ 半径(sphere/cylinder/cone) + f32 height = 1.0f; // 圆柱/圆锥高(沿 +Y,几何中心在原点) + f32 majorRadius = 0.5f; // 圆环主半径 + f32 minorRadius = 0.15f; // 圆环管半径 + u32 segments = 32; // 环绕细分(球/柱/锥/圆环主环) + u32 rings = 16; // 纵向细分(球)/ 圆环管环细分 + u32 subdivisions = 1; // 平面每轴网格细分 + }; + + // 预制图元的 CPU 几何(渲染 Mesh 与物理/脚本共用)。 + struct MeshData { + std::vector vertices; + std::vector indices; // LOD0(三角形列表) + }; + + class MeshFactory { + public: + // 按名字取形状(ShapeNames 中任一);未知名字返回空 MeshData。 + static MeshData Make(const std::string& shape, const PrimitiveDesc& desc); + + // ---- 具名构造(编辑器下拉 / 物理形状 / 脚本可直接调用)---- + static MeshData Cube(f32 halfSize); + static MeshData Plane(f32 halfSize, u32 subdivisions = 1); + static MeshData Sphere(f32 radius, u32 segments = 32, u32 rings = 16); + static MeshData Cylinder(f32 radius, f32 height, u32 segments = 32); + static MeshData Cone(f32 radius, f32 height, u32 segments = 32); + static MeshData Torus(f32 majorRadius, f32 minorRadius, + u32 majorSegments = 32, u32 minorSegments = 16); + + // 已知图元名(UI 下拉 / 伪路径校验的唯一定义点) + static const std::vector& ShapeNames(); + static bool IsKnownShape(const std::string& shape); + + private: + // 逐三角面积加权累积切线 → 对 N 正交化 → 归一(退化取正交兜底) + static void ComputeTangents(MeshData& mesh); + }; + +} // namespace Engine + +#endif //HELLOVULKAN_MESHFACTORY_H diff --git a/Engine/include/Engine/Renderer/Model.h b/Engine/include/Engine/Renderer/Model.h index 949e881..34ba6a5 100644 --- a/Engine/include/Engine/Renderer/Model.h +++ b/Engine/include/Engine/Renderer/Model.h @@ -10,6 +10,7 @@ #include #include #include +#include // 程序化几何唯一源(MeshData/PrimitiveDesc) #include #include #include // 默认参数需完整类型(烘焙节点变换用) @@ -150,9 +151,12 @@ namespace Engine vk::DescriptorSetLayout textureLayout, ParsedModel&& parsed); - // 程序化模型:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。 - // size 为半边长(平面总边长 = 2*size)。 + // 程序化几何标签:几何统一来自 MeshFactory(单一几何源), + // 本类只负责把几何 + 纯色 albedo 组装成可渲染 Model struct ProceduralTag {}; + + // 程序化纯色平面:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。 + // size 为半边长(平面总边长 = 2*size)。 static Scope CreatePlane(Device& device, MemoryAllocator& allocator, DescriptorManager& descriptorManager, vk::DescriptorSetLayout textureLayout, @@ -164,15 +168,25 @@ namespace Engine vk::DescriptorSetLayout textureLayout, f32 halfSize, u8 r, u8 g, u8 b, u8 a); - // 程序化立方体(6 面 24 顶点纯色贴图,halfSize = 半边长): + // 程序化立方体(halfSize = 半边长,几何走 MeshFactory::Cube): // 通用占位几何(碎片模拟的墙/块等)。尺寸变化走实体 Transform // 缩放——同 halfSize 同色共享一份缓存模型 - struct CubeTag {}; static Scope CreateCube(Device& device, MemoryAllocator& allocator, DescriptorManager& descriptorManager, vk::DescriptorSetLayout textureLayout, f32 halfSize, u8 r, u8 g, u8 b); + // 通用程序化图元(MeshFactory 形状名:cube/plane/sphere/cylinder/ + // cone/torus 等):几何由 MeshFactory::Make 生成,纯色 albedo。 + // 非法形状名返回 nullptr(调用方回退) + static Scope CreatePrimitive(Device& device, + MemoryAllocator& allocator, + DescriptorManager& descriptorManager, + vk::DescriptorSetLayout textureLayout, + const std::string& shape, + const PrimitiveDesc& desc, + u8 r, u8 g, u8 b); + ~Model(); Model(const Model&) = delete; @@ -218,18 +232,14 @@ namespace Engine const std::vector& GetBones() const { return m_Bones; } private: + // 程序化几何骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 顶点/索引 + // 由 MeshFactory 产出(见 .cpp)。name = 编辑器材质槽显示名。 + // 各具名工厂(CreatePlane/Cube/Primitive)都折到这一个 ctor Model(Device& device, MemoryAllocator& allocator, DescriptorManager& descriptorManager, vk::DescriptorSetLayout textureLayout, - ProceduralTag, f32 halfSize, const std::array& rgba, - bool transparent); - - // 程序化立方体骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 由 - // 立方体路径填(6 面,顶点法线朝外) - Model(Device& device, MemoryAllocator& allocator, - DescriptorManager& descriptorManager, - vk::DescriptorSetLayout textureLayout, - CubeTag, f32 halfSize, const std::array& rgba); + ProceduralTag, MeshData&& geo, const std::array& rgba, + bool transparent, const std::string& name); // 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填 Model(Device& device, MemoryAllocator& allocator, diff --git a/Engine/include/Engine/Renderer/VulkanAssetUploader.h b/Engine/include/Engine/Renderer/VulkanAssetUploader.h index 34ac4af..b26f5cb 100644 --- a/Engine/include/Engine/Renderer/VulkanAssetUploader.h +++ b/Engine/include/Engine/Renderer/VulkanAssetUploader.h @@ -38,6 +38,9 @@ namespace Engine { Scope CreateTransparentPlane(f32 halfSize, u8 r, u8 g, u8 b, u8 a) override; Scope CreateCube(f32 halfSize, u8 r, u8 g, u8 b) override; + Scope CreatePrimitive(const std::string& shape, + const PrimitiveDesc& desc, + u8 r, u8 g, u8 b) override; Scope LoadEnvironment(const std::string& path) override; Scope LoadEnvironment(const std::string& path, DecodedEquirect&& decoded) override; diff --git a/Engine/src/Assets/AssetManager.cpp b/Engine/src/Assets/AssetManager.cpp index cd8c75c..4014d07 100644 --- a/Engine/src/Assets/AssetManager.cpp +++ b/Engine/src/Assets/AssetManager.cpp @@ -212,6 +212,33 @@ namespace Engine::Assets { [&](const std::string&) { return m_Uploader.CreateCube(halfSize, r, g, b); }); } + ModelRef AssetManager::CreatePrimitive(const std::string& shape, + const PrimitiveDesc& desc, + u8 r, u8 g, u8 b) + { + // 缓存键 = 伪路径 + 形状相关参数(确定性:同形状同参数同色共享)。 + // 只编码该形状真正使用的字段,避免无关字段变化制造重复模型 + std::string params; + if (shape == "cylinder" || shape == "cone") { + params = std::to_string(desc.size) + "_" + + std::to_string(desc.height); + } else if (shape == "torus") { + params = std::to_string(desc.majorRadius) + "_" + + std::to_string(desc.minorRadius); + } else if (shape == "plane") { + params = std::to_string(desc.size) + "_" + + std::to_string(desc.subdivisions); + } else { + params = std::to_string(desc.size); + } + const std::string key = "procedural://" + shape + "/" + params + "/" + + std::to_string(r) + "," + std::to_string(g) + + "," + std::to_string(b); + return m_Models.Load(key, [&](const std::string&) { + return m_Uploader.CreatePrimitive(shape, desc, r, g, b); + }); + } + EnvRef AssetManager::LoadEnvironment(const std::string& path) { // GPU 生成(compute 预积分)经接口发生在渲染层实现里 diff --git a/Engine/src/Renderer/MeshFactory.cpp b/Engine/src/Renderer/MeshFactory.cpp new file mode 100644 index 0000000..87533a4 --- /dev/null +++ b/Engine/src/Renderer/MeshFactory.cpp @@ -0,0 +1,364 @@ +// +// Created by atdunbg on 2026/10/18. +// +// MeshFactory 实现:参数化图元的顶点/索引生成。 +// +// 所有生成器遵循同一套约定(见头文件契约):法线朝外、CCW 正视、 +// UV 在 [0,1] 内、缝/极点处按需复制顶点以保住 UV 与逐面法线。 +// + +#include + +#include + +#include +#include + +namespace Engine { + + namespace { + // 圆柱/圆锥的顶底盖平面 UV:把 xz 以直径归一投影到 [0,1] + glm::vec2 PlanarCapUV(f32 x, f32 z, f32 radius) + { + const f32 d = radius > 1e-6f ? 2.0f * radius : 1.0f; + return { x / d + 0.5f, z / d + 0.5f }; + } + } + + void MeshFactory::ComputeTangents(MeshData& mesh) + { + std::vector tan(mesh.vertices.size(), glm::vec3(0.0f)); + + for (size_t i = 0; i + 2 < mesh.indices.size(); i += 3) { + const Vertex& v0 = mesh.vertices[mesh.indices[i]]; + const Vertex& v1 = mesh.vertices[mesh.indices[i + 1]]; + const Vertex& v2 = mesh.vertices[mesh.indices[i + 2]]; + + const glm::vec3 e1 = v1.position - v0.position; + const glm::vec3 e2 = v2.position - v0.position; + const glm::vec2 d1 = v1.uv - v0.uv; + const glm::vec2 d2 = v2.uv - v0.uv; + + // 退化三角(极点/缝的零面积片、UV 退化为 0 的帽面)跳过, + // 否则 1/det 会引入 inf 污染整根切线 + const f32 det = d1.x * d2.y - d2.x * d1.y; + if (std::abs(det) < 1e-8f) continue; + + const glm::vec3 t = (e1 * d2.y - e2 * d1.y) / det; + tan[mesh.indices[i]] += t; + tan[mesh.indices[i + 1]] += t; + tan[mesh.indices[i + 2]] += t; + } + + for (size_t i = 0; i < mesh.vertices.size(); ++i) { + Vertex& v = mesh.vertices[i]; + // 与 N 正交化(Gram-Schmidt),退化时取任一正交轴兜底 + glm::vec3 t = tan[i] - v.normal * glm::dot(v.normal, tan[i]); + const f32 len = glm::length(t); + if (len > 1e-6f) { + v.tangent = t / len; + continue; + } + const glm::vec3 axis = + std::abs(v.normal.y) < 0.99f ? glm::vec3(0, 1, 0) + : glm::vec3(1, 0, 0); + v.tangent = glm::normalize(glm::cross(axis, v.normal)); + } + } + + // 轴对齐立方体:6 面各 4 顶点(逐面法线,UV 归一),36 索引 + MeshData MeshFactory::Cube(f32 halfSize) + { + const f32 h = halfSize; + const glm::vec3 P[24] = { + // +Z(前) + {-h,-h, h}, { h,-h, h}, { h, h, h}, {-h, h, h}, + // -Z(后) + { h,-h,-h}, {-h,-h,-h}, {-h, h,-h}, { h, h,-h}, + // +Y(上) + {-h, h, h}, { h, h, h}, { h, h,-h}, {-h, h,-h}, + // -Y(下) + {-h,-h,-h}, { h,-h,-h}, { h,-h, h}, {-h,-h, h}, + // +X(右) + { h,-h, h}, { h,-h,-h}, { h, h,-h}, { h, h, h}, + // -X(左) + {-h,-h,-h}, {-h,-h, h}, {-h, h, h}, {-h, h,-h}, + }; + const glm::vec3 N[6] = { + {0, 0, 1}, {0, 0, -1}, {0, 1, 0}, {0, -1, 0}, {1, 0, 0}, {-1, 0, 0}, + }; + const glm::vec2 UV[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; + + MeshData m; + m.vertices.reserve(24); + for (u32 f = 0; f < 6; ++f) { + for (u32 i = 0; i < 4; ++i) { + m.vertices.push_back({P[f * 4 + i], N[f], UV[i]}); + } + } + m.indices.reserve(36); + for (u32 f = 0; f < 6; ++f) { + const u32 b = f * 4; + m.indices.insert(m.indices.end(), {b, b + 1, b + 2, b, b + 2, b + 3}); + } + ComputeTangents(m); + return m; + } + + // XZ 平面:法线 +Y,网格细分 subdivisions(1 = 单四边形) + MeshData MeshFactory::Plane(f32 halfSize, u32 subdivisions) + { + const u32 n = std::max(1u, subdivisions); + const u32 stride = n + 1; + const f32 step = (2.0f * halfSize) / static_cast(n); + + MeshData m; + m.vertices.reserve(stride * stride); + for (u32 r = 0; r <= n; ++r) { + for (u32 c = 0; c <= n; ++c) { + const f32 x = -halfSize + static_cast(c) * step; + const f32 z = -halfSize + static_cast(r) * step; + m.vertices.push_back({ + {x, 0.0f, z}, {0, 1, 0}, + {static_cast(c) / static_cast(n), + static_cast(r) / static_cast(n)}}); + } + } + for (u32 r = 0; r < n; ++r) { + for (u32 c = 0; c < n; ++c) { + const u32 a = r * stride + c; + const u32 b = a + 1; + const u32 cc = a + stride; + const u32 d = cc + 1; + // 绕序令法线朝 +Y((a,cc,d) 与 (a,d,b) 叉积均为 +Y) + m.indices.insert(m.indices.end(), {a, cc, d, a, d, b}); + } + } + ComputeTangents(m); + return m; + } + + // UV 球:极点/缝复制顶点保住 UV,北极为 +Y + MeshData MeshFactory::Sphere(f32 radius, u32 segments, u32 rings) + { + const u32 seg = std::max(3u, segments); + const u32 ring = std::max(2u, rings); + const f32 twoPi = glm::two_pi(); + + MeshData m; + m.vertices.reserve((ring + 1) * (seg + 1)); + for (u32 r = 0; r <= ring; ++r) { + const f32 v = static_cast(r) / static_cast(ring); + const f32 theta = v * glm::pi(); // 0 = +Y 极 + const f32 st = std::sin(theta); + const f32 ct = std::cos(theta); + for (u32 s = 0; s <= seg; ++s) { + const f32 u = static_cast(s) / static_cast(seg); + const f32 phi = u * twoPi; + const glm::vec3 n(st * std::cos(phi), ct, st * std::sin(phi)); + m.vertices.push_back({n * radius, n, {u, 1.0f - v}}); + } + } + m.indices.reserve(ring * seg * 6); + for (u32 r = 0; r < ring; ++r) { + for (u32 s = 0; s < seg; ++s) { + const u32 a = r * (seg + 1) + s; + const u32 b = a + 1; + const u32 c = a + (seg + 1); + const u32 d = c + 1; + m.indices.insert(m.indices.end(), {a, b, d, a, d, c}); + } + } + ComputeTangents(m); + return m; + } + + // 圆柱:侧壁逐列上下两点(法线径向)+ 顶底盖(平面 UV,独立顶点) + MeshData MeshFactory::Cylinder(f32 radius, f32 height, u32 segments) + { + const u32 seg = std::max(3u, segments); + const f32 hh = height * 0.5f; + const f32 twoPi = glm::two_pi(); + + MeshData m; + m.vertices.reserve((seg + 1) * 2 + (seg + 1) * 2 + 2); + + // 侧壁:s 列推 2 顶点(下、上),缝处复制一列 + for (u32 s = 0; s <= seg; ++s) { + const f32 u = static_cast(s) / static_cast(seg); + const f32 phi = u * twoPi; + const f32 cp = std::cos(phi); + const f32 sp = std::sin(phi); + const glm::vec3 n(cp, 0.0f, sp); + m.vertices.push_back( + {{radius * cp, -hh, radius * sp}, n, {u, 0.0f}}); + m.vertices.push_back( + {{radius * cp, hh, radius * sp}, n, {u, 1.0f}}); + } + for (u32 s = 0; s < seg; ++s) { + const u32 a = s * 2; // 下 s + const u32 t = a + 1; // 上 s + const u32 b = a + 2; // 下 s+1 + const u32 d = a + 3; // 上 s+1 + m.indices.insert(m.indices.end(), {a, d, b, a, t, d}); + } + + // 顶盖(+Y) + const u32 topCenter = static_cast(m.vertices.size()); + m.vertices.push_back({{0.0f, hh, 0.0f}, {0, 1, 0}, {0.5f, 0.5f}}); + for (u32 s = 0; s <= seg; ++s) { + const f32 phi = static_cast(s) / static_cast(seg) * twoPi; + const f32 x = radius * std::cos(phi); + const f32 z = radius * std::sin(phi); + m.vertices.push_back({{x, hh, z}, {0, 1, 0}, PlanarCapUV(x, z, radius)}); + } + for (u32 s = 0; s < seg; ++s) { + const u32 r0 = topCenter + 1 + s; + const u32 r1 = topCenter + 1 + s + 1; + m.indices.insert(m.indices.end(), {topCenter, r1, r0}); + } + + // 底盖(-Y) + const u32 botCenter = static_cast(m.vertices.size()); + m.vertices.push_back({{0.0f, -hh, 0.0f}, {0, -1, 0}, {0.5f, 0.5f}}); + for (u32 s = 0; s <= seg; ++s) { + const f32 phi = static_cast(s) / static_cast(seg) * twoPi; + const f32 x = radius * std::cos(phi); + const f32 z = radius * std::sin(phi); + m.vertices.push_back({{x, -hh, z}, {0, -1, 0}, PlanarCapUV(x, z, radius)}); + } + for (u32 s = 0; s < seg; ++s) { + const u32 r0 = botCenter + 1 + s; + const u32 r1 = botCenter + 1 + s + 1; + m.indices.insert(m.indices.end(), {botCenter, r0, r1}); + } + + ComputeTangents(m); + return m; + } + + // 圆锥:底环 + 尖端(每段复制尖端以带倾角法线,每段一个三角)+ 底盖 + MeshData MeshFactory::Cone(f32 radius, f32 height, u32 segments) + { + const u32 seg = std::max(3u, segments); + const f32 baseY = -height * 0.5f; + const f32 apexY = height * 0.5f; + const f32 twoPi = glm::two_pi(); + + MeshData m; + m.vertices.reserve((seg + 1) * 2 + (seg + 1) + 1); + + // 底环(侧壁用,法线含倾角)+ 逐段尖端 + const u32 baseStart = 0; + for (u32 s = 0; s <= seg; ++s) { + const f32 u = static_cast(s) / static_cast(seg); + const f32 phi = u * twoPi; + const f32 cp = std::cos(phi); + const f32 sp = std::sin(phi); + // 侧面法线:竖直分量与半径分量按高/半径配比(高 → 近径向) + const glm::vec3 n = glm::normalize(glm::vec3(height * cp, radius, + height * sp)); + m.vertices.push_back({{radius * cp, baseY, radius * sp}, n, {u, 0.0f}}); + } + const u32 apexStart = static_cast(m.vertices.size()); + for (u32 s = 0; s <= seg; ++s) { + const f32 u = static_cast(s) / static_cast(seg); + const f32 phi = u * twoPi; + const f32 cp = std::cos(phi); + const f32 sp = std::sin(phi); + const glm::vec3 n = glm::normalize(glm::vec3(height * cp, radius, + height * sp)); + m.vertices.push_back({{0.0f, apexY, 0.0f}, n, {u, 1.0f}}); + } + for (u32 s = 0; s < seg; ++s) { + const u32 b0 = baseStart + s; + const u32 b1 = baseStart + s + 1; + const u32 ap = apexStart + s; + m.indices.insert(m.indices.end(), {b0, ap, b1}); + } + + // 底盖(-Y):独立环顶点(平面法线/UV) + const u32 botCenter = static_cast(m.vertices.size()); + m.vertices.push_back({{0.0f, baseY, 0.0f}, {0, -1, 0}, {0.5f, 0.5f}}); + for (u32 s = 0; s <= seg; ++s) { + const f32 phi = static_cast(s) / static_cast(seg) * twoPi; + const f32 x = radius * std::cos(phi); + const f32 z = radius * std::sin(phi); + m.vertices.push_back( + {{x, baseY, z}, {0, -1, 0}, PlanarCapUV(x, z, radius)}); + } + for (u32 s = 0; s < seg; ++s) { + const u32 r0 = botCenter + 1 + s; + const u32 r1 = botCenter + 1 + s + 1; + m.indices.insert(m.indices.end(), {botCenter, r0, r1}); + } + + ComputeTangents(m); + return m; + } + + // 圆环(甜甜圈):主环绕 +Y,管截面绕环切线;两向均闭合成缝复制 + MeshData MeshFactory::Torus(f32 majorRadius, f32 minorRadius, + u32 majorSegments, u32 minorSegments) + { + const u32 ms = std::max(3u, majorSegments); + const u32 ns = std::max(3u, minorSegments); + const f32 twoPi = glm::two_pi(); + + MeshData m; + m.vertices.reserve((ms + 1) * (ns + 1)); + for (u32 i = 0; i <= ms; ++i) { + const f32 u = static_cast(i) / static_cast(ms); + const f32 phi = u * twoPi; + const f32 cp = std::cos(phi); + const f32 sp = std::sin(phi); + for (u32 j = 0; j <= ns; ++j) { + const f32 v = static_cast(j) / static_cast(ns); + const f32 psi = v * twoPi; + const f32 cq = std::cos(psi); + const f32 sq = std::sin(psi); + const glm::vec3 n(cq * cp, sq, cq * sp); + const f32 ringR = majorRadius + minorRadius * cq; + const glm::vec3 p(ringR * cp, minorRadius * sq, ringR * sp); + m.vertices.push_back({p, n, {u, v}}); + } + } + for (u32 i = 0; i < ms; ++i) { + for (u32 j = 0; j < ns; ++j) { + const u32 a = i * (ns + 1) + j; + const u32 b = (i + 1) * (ns + 1) + j; + const u32 c = i * (ns + 1) + j + 1; + const u32 d = (i + 1) * (ns + 1) + j + 1; + m.indices.insert(m.indices.end(), {a, c, d, a, d, b}); + } + } + ComputeTangents(m); + return m; + } + + MeshData MeshFactory::Make(const std::string& shape, const PrimitiveDesc& d) + { + if (shape == "cube") return Cube(d.size); + if (shape == "plane") return Plane(d.size, d.subdivisions); + if (shape == "sphere") return Sphere(d.size, d.segments, d.rings); + if (shape == "cylinder") return Cylinder(d.size, d.height, d.segments); + if (shape == "cone") return Cone(d.size, d.height, d.segments); + if (shape == "torus") + return Torus(d.majorRadius, d.minorRadius, d.segments, d.rings); + return {}; + } + + const std::vector& MeshFactory::ShapeNames() + { + static const std::vector kNames = { + "cube", "plane", "sphere", "cylinder", "cone", "torus"}; + return kNames; + } + + bool MeshFactory::IsKnownShape(const std::string& shape) + { + const auto& names = ShapeNames(); + return std::find(names.begin(), names.end(), shape) != names.end(); + } + +} // namespace Engine diff --git a/Engine/src/Renderer/Model.cpp b/Engine/src/Renderer/Model.cpp index 6481a2e..0722f13 100644 --- a/Engine/src/Renderer/Model.cpp +++ b/Engine/src/Renderer/Model.cpp @@ -805,8 +805,8 @@ void Model::InitDefaultTextures() Model::Model(Device& device, MemoryAllocator& allocator, DescriptorManager& descriptorManager, vk::DescriptorSetLayout textureLayout, - ProceduralTag, f32 halfSize, const std::array& rgba, - bool transparent) + ProceduralTag, MeshData&& geo, const std::array& rgba, + bool transparent, const std::string& name) : m_Device(device) , m_Allocator(allocator) , m_DescriptorManager(descriptorManager) @@ -825,16 +825,9 @@ Model::Model(Device& device, MemoryAllocator& allocator, std::to_string(rgba[3]); m_TextureCache.emplace(texKey, std::move(tex)); - // XZ 平面,法线 +Y,从上往下看 CCW 绕序 - const f32 h = halfSize; - std::vector vertices = { - {{-h, 0.0f, -h}, {0, 1, 0}, {0.0f, 0.0f}}, - {{-h, 0.0f, h}, {0, 1, 0}, {0.0f, 0.0f}}, - {{ h, 0.0f, h}, {0, 1, 0}, {0.0f, 0.0f}}, - {{ h, 0.0f, -h}, {0, 1, 0}, {0.0f, 0.0f}}, - }; - const std::vector indices = {0, 1, 2, 0, 2, 3}; - const std::vector> lodIndices = {indices}; + // 几何(顶点/索引 + 逐顶点切线)由 MeshFactory 产出:单一几何源, + // 各具名工厂不再各自内联一套立方体/平面 + const std::vector> lodIndices = {std::move(geo.indices)}; // 材质/纹理生命周期随模型(缓存持有);颜色纹理必须显式覆盖 albedo // 槽——缺了会折到默认白纹理(白板叠白地 → 平面“消失”的教训) @@ -845,11 +838,9 @@ Model::Model(Device& device, MemoryAllocator& allocator, overrides); m_Meshes.push_back(CreateScope( - m_Device, m_Allocator, vertices, lodIndices, mat)); - // 程序化平面同样登记内建材质快照(与 m_Meshes 下标对齐, - // 编辑器材质槽 UI 不越界) - m_MeshMaterialInfos.push_back( - {"Plane", {}, MaterialParams{}, transparent}); + m_Device, m_Allocator, geo.vertices, lodIndices, mat)); + // 内建材质快照(与 m_Meshes 下标对齐,编辑器材质槽 UI 不越界) + m_MeshMaterialInfos.push_back({name, {}, MaterialParams{}, transparent}); RebuildUnionBounds(); } @@ -860,8 +851,9 @@ Scope Model::CreatePlane(Device& device, MemoryAllocator& allocator, { return Scope(new Model(device, allocator, descriptorManager, textureLayout, ProceduralTag{}, - halfSize, std::array{r, g, b, 255}, - false)); + MeshFactory::Plane(halfSize), + std::array{r, g, b, 255}, false, + "Plane")); } Scope Model::CreateTransparentPlane( @@ -872,8 +864,9 @@ Scope Model::CreateTransparentPlane( { return Scope(new Model(device, allocator, descriptorManager, textureLayout, ProceduralTag{}, - halfSize, std::array{r, g, b, a}, - true)); + MeshFactory::Plane(halfSize), + std::array{r, g, b, a}, true, + "Plane")); } Scope Model::CreateCube(Device& device, MemoryAllocator& allocator, @@ -882,81 +875,36 @@ Scope Model::CreateCube(Device& device, MemoryAllocator& allocator, f32 halfSize, u8 r, u8 g, u8 b) { return Scope(new Model(device, allocator, descriptorManager, - textureLayout, CubeTag{}, - halfSize, std::array{r, g, b, 255})); + textureLayout, ProceduralTag{}, + MeshFactory::Cube(halfSize), + std::array{r, g, b, 255}, false, + "Cube")); } -// 程序化立方体:6 面 24 顶点(面法线朝外,CCW 从外看),36 索引。 -// 结构镜像平面 ctor:默认纹理 → 纯色 albedo 覆盖 → 单 mesh -Model::Model(Device& device, MemoryAllocator& allocator, - DescriptorManager& descriptorManager, - vk::DescriptorSetLayout textureLayout, - CubeTag, f32 halfSize, const std::array& rgba) - : m_Device(device) - , m_Allocator(allocator) - , m_DescriptorManager(descriptorManager) - , m_TextureLayout(textureLayout) +// 通用程序化图元:形状名 → MeshFactory 几何。非法形状名/空几何返回 +// nullptr(调用方回退,绝不把空 mesh 塞进 Mesh) +Scope Model::CreatePrimitive(Device& device, MemoryAllocator& allocator, + DescriptorManager& descriptorManager, + vk::DescriptorSetLayout textureLayout, + const std::string& shape, + const PrimitiveDesc& desc, + u8 r, u8 g, u8 b) { - // 纯色贴图直供 albedo 槽;生命周期挂进纹理缓存(键含 rgba 值, - // 同色共享、异色互不冲突)——与平面 ctor 同一套 - InitDefaultTextures(); - Scope tex = Texture::CreateSolidColor(device, allocator, - rgba[0], rgba[1], rgba[2]); - Texture* albedoOverride = tex.get(); - const std::string texKey = "proc-cube:" + std::to_string(rgba[0]) + ',' + - std::to_string(rgba[1]) + ',' + std::to_string(rgba[2]) + ',' + - std::to_string(rgba[3]); - m_TextureCache.emplace(texKey, std::move(tex)); + if (!MeshFactory::IsKnownShape(shape)) return nullptr; + MeshData geo = MeshFactory::Make(shape, desc); + if (geo.vertices.empty() || geo.indices.empty()) return nullptr; - const f32 h = halfSize; - // 逐面 4 顶点(底左/底右/顶右/顶左,从面外看)+ 单位 UV - const glm::vec3 P[24] = { - // +Z(前) - {-h,-h, h}, { h,-h, h}, { h, h, h}, {-h, h, h}, - // -Z(后) - { h,-h,-h}, {-h,-h,-h}, {-h, h,-h}, { h, h,-h}, - // +Y(上) - {-h, h, h}, { h, h, h}, { h, h,-h}, {-h, h,-h}, - // -Y(下) - {-h,-h,-h}, { h,-h,-h}, { h,-h, h}, {-h,-h, h}, - // +X(右) - { h,-h, h}, { h,-h,-h}, { h, h,-h}, { h, h, h}, - // -X(左) - {-h,-h,-h}, {-h,-h, h}, {-h, h, h}, {-h, h,-h}, - }; - const glm::vec3 N[6] = { - {0, 0, 1}, {0, 0, -1}, {0, 1, 0}, {0, -1, 0}, {1, 0, 0}, {-1, 0, 0}, - }; - const glm::vec2 UV[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}}; - - std::vector vertices; - vertices.reserve(24); - for (u32 f = 0; f < 6; ++f) { - for (u32 i = 0; i < 4; ++i) { - vertices.push_back({P[f * 4 + i], N[f], UV[i]}); - } + // 显示名 = 形状名首字母大写(编辑器材质槽显示用) + std::string name = shape; + if (!name.empty()) { + name[0] = static_cast(std::toupper( + static_cast(name[0]))); } - std::vector indices; - indices.reserve(36); - for (u32 f = 0; f < 6; ++f) { - const u32 b = f * 4; - const std::vector quad = {b, b + 1, b + 2, b, b + 2, b + 3}; - indices.insert(indices.end(), quad.begin(), quad.end()); - } - const std::vector> lodIndices = {indices}; - - std::string paths[Material::kSlotCount] = {"", "", "", ""}; - std::array overrides{}; - overrides[Material::Albedo] = albedoOverride; - Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, false, - overrides); - - m_Meshes.push_back(CreateScope( - m_Device, m_Allocator, vertices, lodIndices, mat)); - // 内建材质快照(与 m_Meshes 下标对齐,编辑器材质槽 UI 不越界) - m_MeshMaterialInfos.push_back( - {"Cube", {}, MaterialParams{}, false}); - RebuildUnionBounds(); + return Scope(new Model(device, allocator, descriptorManager, + textureLayout, ProceduralTag{}, + std::move(geo), + std::array{r, g, b, 255}, false, + name)); } // 资产显示名:文件 stem;无信息量(空 / 导入资产库的 "asset")时读 diff --git a/Engine/src/Renderer/VulkanAssetUploader.cpp b/Engine/src/Renderer/VulkanAssetUploader.cpp index f9bc707..23a8aba 100644 --- a/Engine/src/Renderer/VulkanAssetUploader.cpp +++ b/Engine/src/Renderer/VulkanAssetUploader.cpp @@ -101,6 +101,14 @@ namespace Engine { m_TextureLayout, halfSize, r, g, b); } + Scope VulkanAssetUploader::CreatePrimitive(const std::string& shape, + const PrimitiveDesc& desc, + u8 r, u8 g, u8 b) + { + return Model::CreatePrimitive(m_Device, m_Allocator, m_Descriptors, + m_TextureLayout, shape, desc, r, g, b); + } + Scope VulkanAssetUploader::LoadEnvironment(const std::string& path) { // compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT diff --git a/Engine/src/Serialization/SceneSerializer.cpp b/Engine/src/Serialization/SceneSerializer.cpp index 2209e83..3455f7b 100644 --- a/Engine/src/Serialization/SceneSerializer.cpp +++ b/Engine/src/Serialization/SceneSerializer.cpp @@ -1290,6 +1290,57 @@ namespace Engine::Serialization { catch (const std::exception&) { return {}; } } + // ---- 通用图元(MeshFactory 形状)---- + // 伪路径:"procedural:///{p0[_p1]}/{r},{g},{b}",参数按 '_' + // 拆分。参数个数按形状校验,非法即拒(回退空句柄) + auto tryPrimitive = [&](std::string_view prefix, const std::string& shape, + size_t paramCount) -> ModelRef { + const std::string rest = path.substr(prefix.size()); + const auto slash = rest.find('/'); + if (slash == std::string::npos) return {}; + + const std::string paramStr = rest.substr(0, slash); + int c[3]; + if (!ParseCsvInts(rest.substr(slash + 1), c)) return {}; + + std::vector vals; + size_t pos = 0; + while (true) { + const size_t us = paramStr.find('_', pos); + const std::string tok = paramStr.substr( + pos, us == std::string::npos ? std::string::npos : us - pos); + if (tok.empty()) return {}; + try { vals.push_back(std::stof(tok)); } + catch (const std::exception&) { return {}; } + if (us == std::string::npos) break; + pos = us + 1; + } + if (vals.size() != paramCount) return {}; + + PrimitiveDesc d; + if (shape == "cylinder" || shape == "cone") { + d.size = vals[0]; + d.height = vals[1]; + } else if (shape == "torus") { + d.majorRadius = vals[0]; + d.minorRadius = vals[1]; + } else { + d.size = vals[0]; + } + return assets.CreatePrimitive(shape, d, static_cast(c[0]), + static_cast(c[1]), + static_cast(c[2])); + }; + + if (path.rfind("procedural://sphere/", 0) == 0) + return tryPrimitive("procedural://sphere/", "sphere", 1); + if (path.rfind("procedural://cylinder/", 0) == 0) + return tryPrimitive("procedural://cylinder/", "cylinder", 2); + if (path.rfind("procedural://cone/", 0) == 0) + return tryPrimitive("procedural://cone/", "cone", 2); + if (path.rfind("procedural://torus/", 0) == 0) + return tryPrimitive("procedural://torus/", "torus", 2); + return {}; // 未知伪路径 }