MeshFactory:程序化几何单一源 + CreatePrimitive 链路

新增 MeshFactory(Cube/Plane/Sphere/Cylinder/Cone/Torus + 切线计算),
Model 程序化几何改走 MeshFactory(收敛为单一几何源,消除重复的顶点
生成),并新增 CreatePrimitive;打通 AssetManager/IAssetUploader/
VulkanAssetUploader 的 CreatePrimitive 链路;编辑器 Procedural 弹窗
下拉 + ResolveProcedural 伪路径反解 + 场景序列化(程序化图元可存取)。
This commit is contained in:
2026-10-18 17:09:25 +08:00
parent 22309d24f0
commit f107854236
11 changed files with 654 additions and 117 deletions
+54 -12
View File
@@ -686,8 +686,13 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
} }
if (ImGui::BeginPopup("##proc_model")) if (ImGui::BeginPopup("##proc_model"))
{ {
static int shape = 0; // 0=cube 1=plane 2=transparent plane // 形状集合与 MeshFactory::ShapeNames 对齐
static float half = 0.5f; 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 col[3] = {0.55f, 0.36f, 0.28f};
static float alpha = 0.5f; static float alpha = 0.5f;
ImGui::RadioButton("Cube", &shape, 0); ImGui::RadioButton("Cube", &shape, 0);
@@ -695,11 +700,32 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
ImGui::RadioButton("Plane", &shape, 1); ImGui::RadioButton("Plane", &shape, 1);
ImGui::SameLine(); ImGui::SameLine();
ImGui::RadioButton("Trans.Plane", &shape, 2); 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(撑满可用 // 固定宽度而非 -1:弹窗是 AlwaysAutoResize,-1(撑满可用
// 宽度)会拿上一帧宽度当基准 → 逐帧膨胀直到撑满屏幕 // 宽度)会拿上一帧宽度当基准 → 逐帧膨胀直到撑满屏幕
ImGui::SetNextItemWidth(180.0f); ImGui::SetNextItemWidth(180.0f);
ImGui::SliderFloat("half size", &half, 0.01f, 10.0f, 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"); "%.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); ImGui::ColorEdit3("color", col);
if (shape == 2) 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 r = int(col[0] * 255.0f + 0.5f);
const int g = int(col[1] * 255.0f + 0.5f); const int g = int(col[1] * 255.0f + 0.5f);
const int b = int(col[2] * 255.0f + 0.5f); const int b = int(col[2] * 255.0f + 0.5f);
char key[96]; char key[128];
if (shape == 2) switch (shape)
{ {
const int a = int(alpha * 255.0f + 0.5f); case 2:
std::snprintf( std::snprintf(
key, sizeof(key), key, sizeof(key),
"procedural://transparent-plane/%.3g/%d,%d,%d,%d", "procedural://transparent-plane/%.3g/%d,%d,%d,%d",
half, r, g, b, a); half, r, g, b, int(alpha * 255.0f + 0.5f));
} break;
else case 3:
{ std::snprintf(key, sizeof(key),
std::snprintf( "procedural://sphere/%.3g/%d,%d,%d",
key, sizeof(key), 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", "procedural://%s/%.3g/%d,%d,%d",
shape == 0 ? "cube" : "plane", half, r, g, b); shape == 0 ? "cube" : "plane", half, r, g, b);
break;
} }
if (auto ref = Engine::Serialization:: if (auto ref = Engine::Serialization::
SceneSerializer::ResolveProcedural( SceneSerializer::ResolveProcedural(
@@ -229,6 +229,12 @@ namespace Engine
// 尺寸变化走实体 Transform 缩放,勿按尺寸炸缓存 // 尺寸变化走实体 Transform 缩放,勿按尺寸炸缓存
ModelRef CreateCube(f32 halfSize, u8 r, u8 g, u8 b); 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 预积分四件套)。路径缓存去重, // 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
// 重复 Load 同一路径零成本(动态切换场景直接反复调用) // 重复 Load 同一路径零成本(动态切换场景直接反复调用)
EnvRef LoadEnvironment(const std::string& path); EnvRef LoadEnvironment(const std::string& path);
@@ -45,6 +45,12 @@ namespace Engine::Assets {
// 程序化纯色立方体(halfSize 为半边长;占位几何/碎片模拟) // 程序化纯色立方体(halfSize 为半边长;占位几何/碎片模拟)
virtual Scope<Model> CreateCube(f32 halfSize, u8 r, u8 g, u8 b) = 0; virtual Scope<Model> CreateCube(f32 halfSize, u8 r, u8 g, u8 b) = 0;
// 通用程序化图元(MeshFactory 形状名:cube/plane/sphere/cylinder/
// cone/torus 等):形状 + 参数 → 单 mesh 模型。非法形状返回 nullptr
virtual Scope<Model> CreatePrimitive(const std::string& shape,
const PrimitiveDesc& desc,
u8 r, u8 g, u8 b) = 0;
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr) // 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0; virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
@@ -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 <Engine/Core/Types.h>
#include <Engine/Renderer/Vertex.h>
#include <string>
#include <vector>
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<Vertex> vertices;
std::vector<u32> 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<std::string>& ShapeNames();
static bool IsKnownShape(const std::string& shape);
private:
// 逐三角面积加权累积切线 → 对 N 正交化 → 归一(退化取正交兜底)
static void ComputeTangents(MeshData& mesh);
};
} // namespace Engine
#endif //HELLOVULKAN_MESHFACTORY_H
+23 -13
View File
@@ -10,6 +10,7 @@
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Renderer/Material.h> #include <Engine/Renderer/Material.h>
#include <Engine/Renderer/Mesh.h> #include <Engine/Renderer/Mesh.h>
#include <Engine/Renderer/MeshFactory.h> // 程序化几何唯一源(MeshData/PrimitiveDesc)
#include <glm/gtc/quaternion.hpp> #include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用) #include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用)
@@ -150,9 +151,12 @@ namespace Engine
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
ParsedModel&& parsed); ParsedModel&& parsed);
// 程序化模型:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。 // 程序化几何标签:几何统一来自 MeshFactory(单一几何源),
// size 为半边长(平面总边长 = 2*size)。 // 本类只负责把几何 + 纯色 albedo 组装成可渲染 Model
struct ProceduralTag {}; struct ProceduralTag {};
// 程序化纯色平面:单位法线朝上的 XZ 平面(纯色贴图),用于地面等。
// size 为半边长(平面总边长 = 2*size)。
static Scope<Model> CreatePlane(Device& device, MemoryAllocator& allocator, static Scope<Model> CreatePlane(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
@@ -164,15 +168,25 @@ namespace Engine
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b, u8 a); f32 halfSize, u8 r, u8 g, u8 b, u8 a);
// 程序化立方体(6 面 24 顶点纯色贴图,halfSize = 半边长): // 程序化立方体(halfSize = 半边长,几何走 MeshFactory::Cube):
// 通用占位几何(碎片模拟的墙/块等)。尺寸变化走实体 Transform // 通用占位几何(碎片模拟的墙/块等)。尺寸变化走实体 Transform
// 缩放——同 halfSize 同色共享一份缓存模型 // 缩放——同 halfSize 同色共享一份缓存模型
struct CubeTag {};
static Scope<Model> CreateCube(Device& device, MemoryAllocator& allocator, static Scope<Model> CreateCube(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b); f32 halfSize, u8 r, u8 g, u8 b);
// 通用程序化图元(MeshFactory 形状名:cube/plane/sphere/cylinder/
// cone/torus 等):几何由 MeshFactory::Make 生成,纯色 albedo。
// 非法形状名返回 nullptr(调用方回退)
static 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);
~Model(); ~Model();
Model(const Model&) = delete; Model(const Model&) = delete;
@@ -218,18 +232,14 @@ namespace Engine
const std::vector<ModelBone>& GetBones() const { return m_Bones; } const std::vector<ModelBone>& GetBones() const { return m_Bones; }
private: private:
// 程序化几何骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 顶点/索引
// 由 MeshFactory 产出(见 .cpp)。name = 编辑器材质槽显示名。
// 各具名工厂(CreatePlane/Cube/Primitive)都折到这一个 ctor
Model(Device& device, MemoryAllocator& allocator, Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba, ProceduralTag, MeshData&& geo, const std::array<u8, 4>& rgba,
bool transparent); bool transparent, const std::string& name);
// 程序化立方体骨架:只建默认纹理 + 纯色 albedo 覆盖,mesh 由
// 立方体路径填(6 面,顶点法线朝外)
Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
CubeTag, f32 halfSize, const std::array<u8, 4>& rgba);
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填 // 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
Model(Device& device, MemoryAllocator& allocator, Model(Device& device, MemoryAllocator& allocator,
@@ -38,6 +38,9 @@ namespace Engine {
Scope<Model> CreateTransparentPlane(f32 halfSize, Scope<Model> CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a) override; u8 r, u8 g, u8 b, u8 a) override;
Scope<Model> CreateCube(f32 halfSize, u8 r, u8 g, u8 b) override; Scope<Model> CreateCube(f32 halfSize, u8 r, u8 g, u8 b) override;
Scope<Model> CreatePrimitive(const std::string& shape,
const PrimitiveDesc& desc,
u8 r, u8 g, u8 b) override;
Scope<Cubemap> LoadEnvironment(const std::string& path) override; Scope<Cubemap> LoadEnvironment(const std::string& path) override;
Scope<Cubemap> LoadEnvironment(const std::string& path, Scope<Cubemap> LoadEnvironment(const std::string& path,
DecodedEquirect&& decoded) override; DecodedEquirect&& decoded) override;
+27
View File
@@ -212,6 +212,33 @@ namespace Engine::Assets {
[&](const std::string&) { return m_Uploader.CreateCube(halfSize, r, g, b); }); [&](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) EnvRef AssetManager::LoadEnvironment(const std::string& path)
{ {
// GPU 生成(compute 预积分)经接口发生在渲染层实现里 // GPU 生成(compute 预积分)经接口发生在渲染层实现里
+364
View File
@@ -0,0 +1,364 @@
//
// Created by atdunbg on 2026/10/18.
//
// MeshFactory 实现:参数化图元的顶点/索引生成。
//
// 所有生成器遵循同一套约定(见头文件契约):法线朝外、CCW 正视、
// UV 在 [0,1] 内、缝/极点处按需复制顶点以保住 UV 与逐面法线。
//
#include <Engine/Renderer/MeshFactory.h>
#include <glm/gtc/constants.hpp>
#include <algorithm>
#include <cmath>
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<glm::vec3> 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<f32>(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<f32>(c) * step;
const f32 z = -halfSize + static_cast<f32>(r) * step;
m.vertices.push_back({
{x, 0.0f, z}, {0, 1, 0},
{static_cast<f32>(c) / static_cast<f32>(n),
static_cast<f32>(r) / static_cast<f32>(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<f32>();
MeshData m;
m.vertices.reserve((ring + 1) * (seg + 1));
for (u32 r = 0; r <= ring; ++r) {
const f32 v = static_cast<f32>(r) / static_cast<f32>(ring);
const f32 theta = v * glm::pi<f32>(); // 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<f32>(s) / static_cast<f32>(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<f32>();
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<f32>(s) / static_cast<f32>(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<u32>(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<f32>(s) / static_cast<f32>(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<u32>(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<f32>(s) / static_cast<f32>(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<f32>();
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<f32>(s) / static_cast<f32>(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<u32>(m.vertices.size());
for (u32 s = 0; s <= seg; ++s) {
const f32 u = static_cast<f32>(s) / static_cast<f32>(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<u32>(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<f32>(s) / static_cast<f32>(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<f32>();
MeshData m;
m.vertices.reserve((ms + 1) * (ns + 1));
for (u32 i = 0; i <= ms; ++i) {
const f32 u = static_cast<f32>(i) / static_cast<f32>(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<f32>(j) / static_cast<f32>(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<std::string>& MeshFactory::ShapeNames()
{
static const std::vector<std::string> 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
+37 -89
View File
@@ -805,8 +805,8 @@ void Model::InitDefaultTextures()
Model::Model(Device& device, MemoryAllocator& allocator, Model::Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba, ProceduralTag, MeshData&& geo, const std::array<u8, 4>& rgba,
bool transparent) bool transparent, const std::string& name)
: m_Device(device) : m_Device(device)
, m_Allocator(allocator) , m_Allocator(allocator)
, m_DescriptorManager(descriptorManager) , m_DescriptorManager(descriptorManager)
@@ -825,16 +825,9 @@ Model::Model(Device& device, MemoryAllocator& allocator,
std::to_string(rgba[3]); std::to_string(rgba[3]);
m_TextureCache.emplace(texKey, std::move(tex)); m_TextureCache.emplace(texKey, std::move(tex));
// XZ 平面,法线 +Y,从上往下看 CCW 绕序 // 几何(顶点/索引 + 逐顶点切线)由 MeshFactory 产出:单一几何源,
const f32 h = halfSize; // 各具名工厂不再各自内联一套立方体/平面
std::vector<Vertex> vertices = { const std::vector<std::vector<u32>> lodIndices = {std::move(geo.indices)};
{{-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<u32> indices = {0, 1, 2, 0, 2, 3};
const std::vector<std::vector<u32>> lodIndices = {indices};
// 材质/纹理生命周期随模型(缓存持有);颜色纹理必须显式覆盖 albedo // 材质/纹理生命周期随模型(缓存持有);颜色纹理必须显式覆盖 albedo
// 槽——缺了会折到默认白纹理(白板叠白地 → 平面“消失”的教训) // 槽——缺了会折到默认白纹理(白板叠白地 → 平面“消失”的教训)
@@ -845,11 +838,9 @@ Model::Model(Device& device, MemoryAllocator& allocator,
overrides); overrides);
m_Meshes.push_back(CreateScope<Mesh>( m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, vertices, lodIndices, mat)); m_Device, m_Allocator, geo.vertices, lodIndices, mat));
// 程序化平面同样登记内建材质快照(与 m_Meshes 下标对齐, // 内建材质快照(与 m_Meshes 下标对齐,编辑器材质槽 UI 不越界)
// 编辑器材质槽 UI 不越界) m_MeshMaterialInfos.push_back({name, {}, MaterialParams{}, transparent});
m_MeshMaterialInfos.push_back(
{"Plane", {}, MaterialParams{}, transparent});
RebuildUnionBounds(); RebuildUnionBounds();
} }
@@ -860,8 +851,9 @@ Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
{ {
return Scope<Model>(new Model(device, allocator, descriptorManager, return Scope<Model>(new Model(device, allocator, descriptorManager,
textureLayout, ProceduralTag{}, textureLayout, ProceduralTag{},
halfSize, std::array<u8, 4>{r, g, b, 255}, MeshFactory::Plane(halfSize),
false)); std::array<u8, 4>{r, g, b, 255}, false,
"Plane"));
} }
Scope<Model> Model::CreateTransparentPlane( Scope<Model> Model::CreateTransparentPlane(
@@ -872,8 +864,9 @@ Scope<Model> Model::CreateTransparentPlane(
{ {
return Scope<Model>(new Model(device, allocator, descriptorManager, return Scope<Model>(new Model(device, allocator, descriptorManager,
textureLayout, ProceduralTag{}, textureLayout, ProceduralTag{},
halfSize, std::array<u8, 4>{r, g, b, a}, MeshFactory::Plane(halfSize),
true)); std::array<u8, 4>{r, g, b, a}, true,
"Plane"));
} }
Scope<Model> Model::CreateCube(Device& device, MemoryAllocator& allocator, Scope<Model> Model::CreateCube(Device& device, MemoryAllocator& allocator,
@@ -882,81 +875,36 @@ Scope<Model> Model::CreateCube(Device& device, MemoryAllocator& allocator,
f32 halfSize, u8 r, u8 g, u8 b) f32 halfSize, u8 r, u8 g, u8 b)
{ {
return Scope<Model>(new Model(device, allocator, descriptorManager, return Scope<Model>(new Model(device, allocator, descriptorManager,
textureLayout, CubeTag{}, textureLayout, ProceduralTag{},
halfSize, std::array<u8, 4>{r, g, b, 255})); MeshFactory::Cube(halfSize),
std::array<u8, 4>{r, g, b, 255}, false,
"Cube"));
} }
// 程序化立方体:6 面 24 顶点(面法线朝外,CCW 从外看),36 索引。 // 通用程序化图元:形状名 → MeshFactory 几何。非法形状名/空几何返回
// 结构镜像平面 ctor:默认纹理 → 纯色 albedo 覆盖 → 单 mesh // nullptr(调用方回退,绝不把空 mesh 塞进 Mesh)
Model::Model(Device& device, MemoryAllocator& allocator, Scope<Model> Model::CreatePrimitive(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
CubeTag, f32 halfSize, const std::array<u8, 4>& rgba) const std::string& shape,
: m_Device(device) const PrimitiveDesc& desc,
, m_Allocator(allocator) u8 r, u8 g, u8 b)
, m_DescriptorManager(descriptorManager)
, m_TextureLayout(textureLayout)
{ {
// 纯色贴图直供 albedo 槽;生命周期挂进纹理缓存(键含 rgba 值, if (!MeshFactory::IsKnownShape(shape)) return nullptr;
// 同色共享、异色互不冲突)——与平面 ctor 同一套 MeshData geo = MeshFactory::Make(shape, desc);
InitDefaultTextures(); if (geo.vertices.empty() || geo.indices.empty()) return nullptr;
Scope<Texture> 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));
const f32 h = halfSize; // 显示名 = 形状名首字母大写(编辑器材质槽显示用)
// 逐面 4 顶点(底左/底右/顶右/顶左,从面外看)+ 单位 UV std::string name = shape;
const glm::vec3 P[24] = { if (!name.empty()) {
// +Z(前) name[0] = static_cast<char>(std::toupper(
{-h,-h, h}, { h,-h, h}, { h, h, h}, {-h, h, h}, static_cast<unsigned char>(name[0])));
// -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<Vertex> 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]});
} }
} return Scope<Model>(new Model(device, allocator, descriptorManager,
std::vector<u32> indices; textureLayout, ProceduralTag{},
indices.reserve(36); std::move(geo),
for (u32 f = 0; f < 6; ++f) { std::array<u8, 4>{r, g, b, 255}, false,
const u32 b = f * 4; name));
const std::vector<u32> quad = {b, b + 1, b + 2, b, b + 2, b + 3};
indices.insert(indices.end(), quad.begin(), quad.end());
}
const std::vector<std::vector<u32>> lodIndices = {indices};
std::string paths[Material::kSlotCount] = {"", "", "", ""};
std::array<Texture*, Material::kSlotCount> overrides{};
overrides[Material::Albedo] = albedoOverride;
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, false,
overrides);
m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, vertices, lodIndices, mat));
// 内建材质快照(与 m_Meshes 下标对齐,编辑器材质槽 UI 不越界)
m_MeshMaterialInfos.push_back(
{"Cube", {}, MaterialParams{}, false});
RebuildUnionBounds();
} }
// 资产显示名:文件 stem;无信息量(空 / 导入资产库的 "asset")时读 // 资产显示名:文件 stem;无信息量(空 / 导入资产库的 "asset")时读
@@ -101,6 +101,14 @@ namespace Engine {
m_TextureLayout, halfSize, r, g, b); m_TextureLayout, halfSize, r, g, b);
} }
Scope<Model> 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<Cubemap> VulkanAssetUploader::LoadEnvironment(const std::string& path) Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(const std::string& path)
{ {
// compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT // compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT
@@ -1290,6 +1290,57 @@ namespace Engine::Serialization {
catch (const std::exception&) { return {}; } catch (const std::exception&) { return {}; }
} }
// ---- 通用图元(MeshFactory 形状)----
// 伪路径:"procedural://<shape>/{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<f32> 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<u8>(c[0]),
static_cast<u8>(c[1]),
static_cast<u8>(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 {}; // 未知伪路径 return {}; // 未知伪路径
} }